
如题,我在测试 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 年 3 月 18 日 via Android 不考虑线程间通讯的成本的吗,只要你需要统一搜集结果(或者线程同步),就会有通讯成本的问题,这个影响是很大的 除此之外,消费级 cpu 还有超线程的影响 以及多个核心同时工作导致无法同时达到最大睿频 或者干脆笔记本撞功耗墙 |
2 laurencedu 2021 年 3 月 18 日 没有探究过原因,但实际上 python 多线程的效率相比 java 或者 c++是很低的我们团队一般认为 python 的多线程没有效率,不会比单进程快多少。通常如果需要并发执行任务,我们这边都是起多个 python 进程(多个程序)使用不同的参数一起跑。 |
3 love 2021 年 3 月 18 日 你都说 GIL 了,这货不就是干这个用的,一个大锁就相当于就是单线程的解释器,搞多线程的假象只是为了 IO 分片,不是是计算分片 |
4 aydd2004 2021 年 3 月 18 日 via iPhone @laurencedu 原来不只我这种菜鸡这么干 哈哈哈哈 |
5 ysc3839 2021 年 3 月 18 日 via Android 你的想法是不是:单线程执行的时候只使用了一个核心,耗时 T,多线程使用所有核心,但不同核心之间是不影响的,所以耗时也应该是 T ? 我估计是睿频的影响,有空我试试用 C++写一个,并且锁定 CPU 频率看看结果如何。 |
6 wzb0909 2021 年 3 月 18 日 via iPhone 我 tm 就不该把楼主从 block 里放出来 |
7 LeeReamond OP @wzb0909 谢谢,block 了 |
8 vicalloy 2021 年 3 月 18 日 先看一下操作系统的资源占用情况,看看每个 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 年 3 月 18 日 呃,我把你代码在服务器跑了一下,服务器 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 年 3 月 18 日 via Android @LeeReamond 我特意规避 GIL 和进程创建成本就是防着这一手,结果还是防不胜防啊 跨越进程的通讯当然也算是线程通讯,毕竟线程是基本执行单位,只是不能使用进程内的机制而已,这个意义说跨越物理 cpu甚至物理机器的通讯也是线程间通讯。 |
15 vipppppp 2021 年 3 月 18 日 我又在另外 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 年 3 月 18 日 |
17 WinG 2021 年 3 月 18 日 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 年 3 月 18 日 |
22 vipppppp 2021 年 3 月 18 日 2 个核心各占 50% => 不是完全的 50,一个 50 多,一个 40 多, |
23 tusj 2021 年 3 月 18 日 我记得 python 的多线程是假的 |
24 no1xsyzy 2021 年 3 月 18 日 @LeeReamond 你这边就传个函数名称再来回各传个 int,也是在一块芯片里,能有多少的通讯开支…… @vipppppp 跨核心是个有毛病的问题,而且 CPython 都 GIL 了还不想办法限定核心…… 倒也是不强求优化…… 单路 CPU 也可能是双 NUMA 节点( Ryzen 1-2 似乎有?)。 |
25 cherryas 2021 年 3 月 18 日 python 的多线程不是在阻塞的时候才有意义吗? |
26 qianxings 2021 年 3 月 18 日 (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 年 3 月 18 日 @LeeReamond 我刚刚看错了,我那台机器是 128 核的,所以才慢的离谱,哈哈 |
28 LeeReamond OP @no1xsyzy 看到你的讨论想说些题外话。目前 python 指定核心分配进程有可用方案了吗?能想到一个典型场景是 gunicorn 每个进程绑定后线路应该能提高一些。 另外绑定核心这件事应该怎么理解,比如我的主线程绑定到 a 核心,然后我新开了一个线程调用 dll 插件,这个操作过程用释放 gil,那这个并行线程是会另找地方还是只能在当前核心上排队? |
29 LeeReamond OP @LeeReamond 128 核的话,多进程又为啥会是 5x 加速呢,毕竟有 8 个进程。。神秘 |
30 ch2 2021 年 3 月 18 日 |
31 no1xsyzy 2021 年 3 月 18 日 @LeeReamond 我说的指定核心是指进(线?)程核心对应关系,具体操作不记得了,是操作系统?提供的功能。 目前能用的方案大概只有自己手动调整或者调用 syscall 。结果是只能当前核心上排队,确实对于外部库可能释放 GIL 不太友好。不过,CPython 层面理论上有办法实现所有 GIL 在一个核心上处理,一旦进入非 Python 代码导致释放 GIL 锁则放弃该线程的核心绑定。但例如持续变化、难以预测实际开销的情况下 tradeoff 会比较麻烦。另一方面,程序自身改动核心绑定可能会出现意外的情况(比如多个完全无关的程序绑定到同一核心,结果相互竞争资源)。 我注意到这个操作可能是有用的起因,Windows 上有一个 CPU Cores 来强制处理游戏的进程核心对应关系,把操作系统其他内容全部丢给一个核心,其余核心全力跑游戏。但我并没有很多实验数据去理解它以何种方式、在何种条件下有何等程度的作用。 |
32 yazoox 2021 年 3 月 18 日 没用过。只能关注学习一波了。 |
33 linw1995 2021 年 3 月 18 日 os 有用 cgroups 对 cpu 使用率作限制吗?或者说,你有排除这类影响因素吗 |
34 systemcall 2021 年 3 月 18 日 @no1xsyzy #31 没有发现 Windows 的 CPU 调度和游戏是否运行有多大的关系 如果你的 Windows 系统比你的 CPU 新,一般是会正确处理超线程的,但是一个核心的负载不算大的时候可能会因为节能方面的策略把 2 个线程都用起来。这个你拿 hwinfo 之类的可以准确的看到硬件的比较详细的状态的软件可以看出来。 |
35 blackbbc 2021 年 3 月 18 日 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 年 3 月 18 日 via iPhone @laurencedu 你们那什么团队试过多线程和单线程在 io 密集场景的对比 |
37 no1xsyzy 2021 年 3 月 18 日 @systemcall 可能有点不清晰 有一个 Windows 上的第三方软件叫 “CPU Cores”,该软件通过调用系统 API 迫使游戏以外的所有活动分配到单一核心,而为游戏分配其他所有 CPU 。 与游戏的运行与否无关,与游戏运行的性能有关,平均提升 10% (蚊子腿也是肉啊) |
38 ipwx 2021 年 3 月 18 日 via iPhone python 的线程是真的系统线程,只不过有个 gil 所以不会有多个线程同时执行而已。然而只要是真的线程发生了切换,python 解释器的内部状态同步就要让很多 cpu cache 分支预测之类的黑魔法失效。一个简单例子,cpu 从核内缓存读数据是 1ns 级别的,从主存调数据是 100ns 级别的。如果多线程被分在不同核上,那么一个线程改了一个变量,会导致其他核上的这个变量的核内缓存失效,下一次调度就要重新读内存。。。所以大概这也是为啥有些平台上多线程这么差的原因。 |
39 ipwx 2021 年 3 月 18 日 via iPhone 有些系统可能会倾向于让同一个进程的不同线程在相同核上执行,那么影响就会小很多。。。有些没这个意识就遭罪了。最怕的就是同一个线程还在不同核上反复横跳,当然一般都不会有智障操作系统这么搞的,除非负载真的很大 |
40 ipwx 2021 年 3 月 18 日 via iPhone 而且雪上加霜的是,核内缓存 cpu l1-l3 cache 是以 cache line 为单位缓存数据的。我记得 cache line 至少是 64B 。这导致一个变量失效就会让多个变量同时失效。。。() |
41 binux 2021 年 3 月 18 日 via Android 进程间通信开销没有那么大 多核性能并不是单核性能 xN,你去看 CPU benchmark 就能注意到 你代码统计的是最慢的一个进程用时,试试分别计时再加起来? |
42 LeeReamond OP @binux 提这个问题主要是我 8 核才提升 2 倍性能,比较诡异所以来问一下,看大家的跑分感觉可能是我硬件问题,不是 python 或者调用方式有问题。物理 8 核心分配 8 线程,资源足够的情况下执行顺序先后完成时间差别不大,属于比较粗略的计算方式,因为不需要进程间通讯。 |