如题,我在测试 ctypes 释放 GIL 的过程中发现这个问题,即使使用 c 代码将 GIL 释放,多线程并行的效率并不是比如我有 N 个线程那么程序的运算能力就变成 N 倍。即使线程之间完全没有资源竞争问题,这个是令我很意外的一个点。
我觉得可能的原因是线程之间始终要进行一些状态同步,那 OK 我使用多进程总归是完全隔离了吧,结果测试结果没有太大变化,令人大跌眼镜。
我理解上,进程互相之间完全独立,如果你的物理计算资源足够(比如我使用的 CPU 是 8 核心 16 线程的),那么你运行 8 个独立的进程,他们应该是互相完全独立,速度互不干扰的,但实验结果并非如此,请问一下 v 友们之中有没有大佬能解释一下原因,谢谢。
=====
测试代码如下,因为我无法上传 DLL,使用递归菲波那切数列模拟 CPU 密集型任务。这会使多线程执行时间线性增长,但理论不应影响到多进程。另外以下实验代码中使用子进程的方式,我担心可能是子进程状态同步导致的效率损失,但实际手动在 shell 中启动多个不同进程,实验结果没有区别。
以下使用的进程池 /线程池都经过了预激。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
import time
def pre_activate(times):
time.sleep(times)
def execution():
def fib(n):
if n<=1:
return 1
return fib(n-1) + fib(n-2)
for i in range(20):
fib(30)
if __name__ == "__main__":
core_num = 8
st_time = time.time()
execution()
single_execute_time = time.time() - st_time
print(f"Single thread execute time: {round(single_execute_time,4)} s")
with ThreadPoolExecutor(max_workers=core_num) as executor:
# pre-activate {core_num} threads in threadpoolexecutor
pre_task = [executor.submit(pre_activate, times) \
for times in [0.5 for _ in range(core_num)]]
for future in as_completed(pre_task):future.result()
st_time = time.time()
tasks = [executor.submit(execution) for _ in range(core_num)]
for future in as_completed(tasks):future.result()
print(f"Multi thread execute time: {round(time.time() - st_time,4)} s",
f", speedup: {round(core_num * single_execute_time / (time.time() - st_time),2)} x")
with ProcessPoolExecutor(max_workers=core_num) as executor:
#
pre_task = [executor.submit(pre_activate, times)
for times in [0.5 for _ in range(core_num)]]
for future in as_completed(pre_task):future.result()
st_time = time.time()
tasks = [executor.submit(execution) for _ in range(core_num)]
for future in as_completed(tasks):future.result()
print(f"Multi Process execute time: {round(time.time() - st_time,4)} s",
f", speedup: {round(core_num * single_execute_time / (time.time() - st_time),2)} x")
我的本地执行结果是:
Single thread execute time: 4.117 s
Multi thread execute time: 32.888 s , speedup: 1.0 x
Multi Process execute time: 12.1088 s , speedup: 2.72 x
无论更换哪些 CPU 密集型任务,speedup 几乎很难提升到 3 倍以上,即使使用 8 核心并行计算,为什么?
这个结果同时让我想起一些以前的跑分经验,比如进入异步时代以后使用 gunicorn 单线程部署一个 web 服务通常 echo 可以做到每秒钟两万次以上,但使用 prefork 的多进程,也不过将这个数值提升 2-2.5 倍,并不能提升很多,以前没有细究,现在觉得不太对
1
codehz 2021-03-18 07:15:48 +08:00 via Android
不考虑线程间通讯的成本的吗,只要你需要统一搜集结果(或者线程同步),就会有通讯成本的问题,这个影响是很大的
除此之外,消费级 cpu 还有超线程的影响 以及多个核心同时工作导致无法同时达到最大睿频 或者干脆笔记本撞功耗墙 |
2
laurencedu 2021-03-18 07:38:23 +08:00
没有探究过原因,但实际上 python 多线程的效率相比 java 或者 c++是很低的——我们团队一般认为 python 的多线程没有效率,不会比单进程快多少。通常如果需要并发执行任务,我们这边都是起多个 python 进程(多个程序)使用不同的参数一起跑。
|
3
love 2021-03-18 07:47:57 +08:00 1
你都说 GIL 了,这货不就是干这个用的,一个大锁就相当于就是单线程的解释器,搞多线程的假象只是为了 IO 分片,不是是计算分片
|
4
aydd2004 2021-03-18 07:50:02 +08:00 via iPhone
@laurencedu 原来不只我这种菜鸡这么干 哈哈哈哈
|
5
ysc3839 2021-03-18 07:55:36 +08:00 via Android 1
你的想法是不是:单线程执行的时候只使用了一个核心,耗时 T,多线程使用所有核心,但不同核心之间是不影响的,所以耗时也应该是 T ?
我估计是睿频的影响,有空我试试用 C++写一个,并且锁定 CPU 频率看看结果如何。 |
6
wzb0909 2021-03-18 08:06:12 +08:00 via iPhone 4
我 tm 就不该把楼主从 block 里放出来
|
7
LeeReamond OP @wzb0909 谢谢,block 了
|
8
vicalloy 2021-03-18 09:06:47 +08:00
先看一下操作系统的资源占用情况,看看每个 CPU 核心的资源占用率。
|
9
LeeReamond OP @love
@laurencedu @codehz 感谢各位回复,不过我帖子中讨论的确实是多进程,并且除了说明以外给出了测试代码及执行结果。并不是各位在讨论的所谓线程效率的问题 我最近确实震惊于程序员群体语文阅读能力之低下,最近几天在 v2 讨论遇到了很多次驴唇不对马嘴的回复,实在不吐不快。 |
10
LeeReamond OP @vicalloy 多进程模式下 16 线程跑 8 进程,其中 8 线程是满载的,剩下占用在 20-60%之间抖动。测试平台 windows,空载状态下运行,我不认为是系统资源不足的影响。
|
11
LeeReamond OP @ysc3839 确实,大佬给出了一个合理的思路。不过如我测试,绝对执行时间增长了三倍,睿频应该差不了这么多吧。
|
12
vipppppp 2021-03-18 09:23:21 +08:00
呃,我把你代码在服务器跑了一下,服务器 256G 内存,64 线程,处于空闲状态
跑你的代码的结果: Single thread execute time: 9.2876 s Multi thread execute time: 224.082 s , speedup: 0.33 x Multi Process execute time: 9.5536 s , speedup: 7.78 x |
13
LeeReamond OP @vipppppp 感谢,看来确实可能是我之前忽略了睿频的问题,不过大佬你这个结果里多进程是符合期望的,多线程在 gil 下顺序执行,不应该这么慢
|
14
codehz 2021-03-18 09:50:04 +08:00 via Android
@LeeReamond 我特意规避 GIL 和进程创建成本就是防着这一手,结果还是防不胜防啊
跨越进程的通讯当然也算是线程通讯,毕竟线程是基本执行单位,只是不能使用进程内的机制而已,这个意义说跨越物理 cpu,甚至物理机器的通讯也是线程间通讯。 |
15
vipppppp 2021-03-18 09:52:41 +08:00
我又在另外 2 台跑了一下,
这个是 12 线程空闲的: Single thread execute time: 5.0026 s Multi thread execute time: 68.0828 s , speedup: 0.59 x Multi Process execute time: 7.8153 s , speedup: 5.12 x 这台是 48 线程基本空闲的: Single thread execute time: 9.4396 s Multi thread execute time: 89.9176 s , speedup: 0.84 x Multi Process execute time: 9.8601 s , speedup: 7.66 x 反正就是结果差别都很大吧。。。 |
16
no1xsyzy 2021-03-18 10:08:02 +08:00
|
17
WinG 2021-03-18 10:12:48 +08:00 via Android
9900k 单核睿频可以跑到 5.1g 全核睿频只有 4.6g
|
18
LeeReamond OP @vipppppp 我无法解释你的跑分结果,虽然这个代码也只是图一乐,并不严谨,但是应该不会影响大方向结论。比如你在超多核的机器上多线程反而特别慢,我觉得有可能是同一段逻辑在不同物理核心上交替运行,期间资源移来移去产生的开销。不过在 12 线程上也这么慢就不合理了,12 线程可不太像是双路 cpu 。。。因为是纯 python 实现,正常多线程的 speedup 就应该是 1.0 左右
|
19
LeeReamond OP @no1xsyzy 刚才群里跟大佬讨论,大佬说你这个进程间通讯时间都没算,测个屁。我倒只是想得出个大方向结论,没想那么精确,不过我觉得在预激的基础上,进程间通讯的开销应该在微秒级,最慢不会超过几毫秒,这不是影响 4 秒执行时间延长到 12 秒的理由
|
21
vipppppp 2021-03-18 10:33:38 +08:00
|
22
vipppppp 2021-03-18 10:34:28 +08:00
2 个核心各占 50% => 不是完全的 50,一个 50 多,一个 40 多,
|
23
tusj 2021-03-18 10:36:09 +08:00
我记得 python 的多线程是假的
|
24
no1xsyzy 2021-03-18 10:45:33 +08:00
@LeeReamond 你这边就传个函数名称再来回各传个 int,也是在一块芯片里,能有多少的通讯开支……
@vipppppp 跨核心是个有毛病的问题,而且 CPython 都 GIL 了还不想办法限定核心…… 倒也是不强求优化…… 单路 CPU 也可能是双 NUMA 节点( Ryzen 1-2 似乎有?)。 |
25
cherryas 2021-03-18 10:46:14 +08:00
python 的多线程不是在阻塞的时候才有意义吗?
|
26
qianxings 2021-03-18 10:50:58 +08:00
(base) [root@bigdata ~]# python a.py
Single thread execute time: 4.8245 s Multi thread execute time: 39.3859 s , speedup: 0.98 x Multi Process execute time: 5.0472 s , speedup: 7.65 x (base) [root@bigdata ~]# lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 座: 1 NUMA 节点: 1 厂商 ID: GenuineIntel CPU 系列: 6 型号: 85 型号名称: Intel(R) Xeon(R) Gold 6266C CPU @ 3.00GHz 步进: 7 CPU MHz: 3000.000 BogoMIPS: 6000.00 超管理器厂商: KVM 虚拟化类型: 完全 L1d 缓存: 32K L1i 缓存: 32K L2 缓存: 1024K L3 缓存: 30976K NUMA 节点 0 CPU: 0-7 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 arat avx512_vnni md_clear spec_ctrl intel_stibp flush_l1d arch_capabilities (base) [root@bigdata ~]# |
27
vipppppp 2021-03-18 10:51:16 +08:00
@LeeReamond
我刚刚看错了,我那台机器是 128 核的,所以才慢的离谱,哈哈 |
28
LeeReamond OP @no1xsyzy 看到你的讨论想说些题外话。目前 python 指定核心分配进程有可用方案了吗?能想到一个典型场景是 gunicorn 每个进程绑定后线路应该能提高一些。
另外绑定核心这件事应该怎么理解,比如我的主线程绑定到 a 核心,然后我新开了一个线程调用 dll 插件,这个操作过程用释放 gil,那这个并行线程是会另找地方还是只能在当前核心上排队? |
29
LeeReamond OP @LeeReamond 128 核的话,多进程又为啥会是 5x 加速呢,毕竟有 8 个进程。。神秘
|
30
ch2 2021-03-18 11:14:24 +08:00
|
31
no1xsyzy 2021-03-18 11:14:57 +08:00 1
@LeeReamond 我说的指定核心是指进(线?)程核心对应关系,具体操作不记得了,是操作系统?提供的功能。
目前能用的方案大概只有自己手动调整或者调用 syscall 。结果是只能当前核心上排队,确实对于外部库可能释放 GIL 不太友好。不过,CPython 层面理论上有办法实现所有 GIL 在一个核心上处理,一旦进入非 Python 代码导致释放 GIL 锁则放弃该线程的核心绑定。但例如持续变化、难以预测实际开销的情况下 tradeoff 会比较麻烦。另一方面,程序自身改动核心绑定可能会出现意外的情况(比如多个完全无关的程序绑定到同一核心,结果相互竞争资源)。 我注意到这个操作可能是有用的起因,Windows 上有一个 CPU Cores 来强制处理游戏的进程核心对应关系,把操作系统其他内容全部丢给一个核心,其余核心全力跑游戏。但我并没有很多实验数据去理解它以何种方式、在何种条件下有何等程度的作用。 |
32
yazoox 2021-03-18 11:48:28 +08:00
没用过。只能关注学习一波了。
|
33
linw1995 2021-03-18 12:28:40 +08:00
os 有用 cgroups 对 cpu 使用率作限制吗?或者说,你有排除这类影响因素吗
|
34
systemcall 2021-03-18 12:34:33 +08:00
@no1xsyzy #31
没有发现 Windows 的 CPU 调度和游戏是否运行有多大的关系 如果你的 Windows 系统比你的 CPU 新,一般是会正确处理超线程的,但是一个核心的负载不算大的时候可能会因为节能方面的策略把 2 个线程都用起来。这个你拿 hwinfo 之类的可以准确的看到硬件的比较详细的状态的软件可以看出来。 |
35
blackbbc 2021-03-18 12:42:25 +08:00
Single thread execute time: 5.0241 s
Multi thread execute time: 40.5616 s , speedup: 0.99 x Multi Process execute time: 5.6483 s , speedup: 7.12 x MacBook Pro 16 2019 跑分结果 |
36
johnsona 2021-03-18 12:45:47 +08:00 via iPhone
@laurencedu 你们那什么团队试过多线程和单线程在 io 密集场景的对比
|
37
no1xsyzy 2021-03-18 12:50:52 +08:00
@systemcall 可能有点不清晰
有一个 Windows 上的第三方软件叫 “CPU Cores”,该软件通过调用系统 API 迫使游戏以外的所有活动分配到单一核心,而为游戏分配其他所有 CPU 。 与游戏的运行与否无关,与游戏运行的性能有关,平均提升 10% (蚊子腿也是肉啊) |
38
ipwx 2021-03-18 12:55:02 +08:00 via iPhone
python 的线程是真的系统线程,只不过有个 gil 所以不会有多个线程同时执行而已。然而只要是真的线程发生了切换,python 解释器的内部状态同步就要让很多 cpu cache 分支预测之类的黑魔法失效。一个简单例子,cpu 从核内缓存读数据是 1ns 级别的,从主存调数据是 100ns 级别的。如果多线程被分在不同核上,那么一个线程改了一个变量,会导致其他核上的这个变量的核内缓存失效,下一次调度就要重新读内存。。。所以大概这也是为啥有些平台上多线程这么差的原因。
|
39
ipwx 2021-03-18 12:56:41 +08:00 via iPhone
有些系统可能会倾向于让同一个进程的不同线程在相同核上执行,那么影响就会小很多。。。有些没这个意识就遭罪了。最怕的就是同一个线程还在不同核上反复横跳,当然一般都不会有智障操作系统这么搞的,除非负载真的很大
|
40
ipwx 2021-03-18 12:58:20 +08:00 via iPhone
而且雪上加霜的是,核内缓存 cpu l1-l3 cache 是以 cache line 为单位缓存数据的。我记得 cache line 至少是 64B 。这导致一个变量失效就会让多个变量同时失效。。。()
|
41
binux 2021-03-18 13:02:59 +08:00 via Android
进程间通信开销没有那么大
多核性能并不是单核性能 xN,你去看 CPU benchmark 就能注意到 你代码统计的是最慢的一个进程用时,试试分别计时再加起来? |
42
LeeReamond OP @binux 提这个问题主要是我 8 核才提升 2 倍性能,比较诡异所以来问一下,看大家的跑分感觉可能是我硬件问题,不是 python 或者调用方式有问题。物理 8 核心分配 8 线程,资源足够的情况下执行顺序先后完成时间差别不大,属于比较粗略的计算方式,因为不需要进程间通讯。
|