有没有人注意观察过, Python 多进程执行同一程序速度比单进程执行慢很多,原因是什么?

2021-03-18 06:58:35 +08:00
 LeeReamond

如题,我在测试 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 倍,并不能提升很多,以前没有细究,现在觉得不太对

1937 次点击
所在节点    Python
42 条回复
vipppppp
2021-03-18 10:33:38 +08:00
@LeeReamond
在其中一台机器上,我测试的时候看了 cpu,多线程指定 2 个 cpu(逻辑)的时候,2 个核心各占 50%。
如果指定一个个的话,那么多线程就是这个的 100%。
vipppppp
2021-03-18 10:34:28 +08:00
2 个核心各占 50% => 不是完全的 50,一个 50 多,一个 40 多,
tusj
2021-03-18 10:36:09 +08:00
我记得 python 的多线程是假的
no1xsyzy
2021-03-18 10:45:33 +08:00
@LeeReamond 你这边就传个函数名称再来回各传个 int,也是在一块芯片里,能有多少的通讯开支……

@vipppppp 跨核心是个有毛病的问题,而且 CPython 都 GIL 了还不想办法限定核心…… 倒也是不强求优化……

单路 CPU 也可能是双 NUMA 节点( Ryzen 1-2 似乎有?)。
cherryas
2021-03-18 10:46:14 +08:00
python 的多线程不是在阻塞的时候才有意义吗?
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 ~]#
vipppppp
2021-03-18 10:51:16 +08:00
@LeeReamond
我刚刚看错了,我那台机器是 128 核的,所以才慢的离谱,哈哈
LeeReamond
2021-03-18 10:58:41 +08:00
@no1xsyzy 看到你的讨论想说些题外话。目前 python 指定核心分配进程有可用方案了吗?能想到一个典型场景是 gunicorn 每个进程绑定后线路应该能提高一些。

另外绑定核心这件事应该怎么理解,比如我的主线程绑定到 a 核心,然后我新开了一个线程调用 dll 插件,这个操作过程用释放 gil,那这个并行线程是会另找地方还是只能在当前核心上排队?
LeeReamond
2021-03-18 11:00:35 +08:00
@LeeReamond 128 核的话,多进程又为啥会是 5x 加速呢,毕竟有 8 个进程。。神秘
ch2
2021-03-18 11:14:24 +08:00
no1xsyzy
2021-03-18 11:14:57 +08:00
@LeeReamond 我说的指定核心是指进(线?)程核心对应关系,具体操作不记得了,是操作系统?提供的功能。
目前能用的方案大概只有自己手动调整或者调用 syscall 。结果是只能当前核心上排队,确实对于外部库可能释放 GIL 不太友好。不过,CPython 层面理论上有办法实现所有 GIL 在一个核心上处理,一旦进入非 Python 代码导致释放 GIL 锁则放弃该线程的核心绑定。但例如持续变化、难以预测实际开销的情况下 tradeoff 会比较麻烦。另一方面,程序自身改动核心绑定可能会出现意外的情况(比如多个完全无关的程序绑定到同一核心,结果相互竞争资源)。

我注意到这个操作可能是有用的起因,Windows 上有一个 CPU Cores 来强制处理游戏的进程核心对应关系,把操作系统其他内容全部丢给一个核心,其余核心全力跑游戏。但我并没有很多实验数据去理解它以何种方式、在何种条件下有何等程度的作用。
yazoox
2021-03-18 11:48:28 +08:00
没用过。只能关注学习一波了。
linw1995
2021-03-18 12:28:40 +08:00
os 有用 cgroups 对 cpu 使用率作限制吗?或者说,你有排除这类影响因素吗
systemcall
2021-03-18 12:34:33 +08:00
@no1xsyzy #31
没有发现 Windows 的 CPU 调度和游戏是否运行有多大的关系
如果你的 Windows 系统比你的 CPU 新,一般是会正确处理超线程的,但是一个核心的负载不算大的时候可能会因为节能方面的策略把 2 个线程都用起来。这个你拿 hwinfo 之类的可以准确的看到硬件的比较详细的状态的软件可以看出来。
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 跑分结果
johnsona
2021-03-18 12:45:47 +08:00
@laurencedu 你们那什么团队试过多线程和单线程在 io 密集场景的对比
no1xsyzy
2021-03-18 12:50:52 +08:00
@systemcall 可能有点不清晰
有一个 Windows 上的第三方软件叫 “CPU Cores”,该软件通过调用系统 API 迫使游戏以外的所有活动分配到单一核心,而为游戏分配其他所有 CPU 。
与游戏的运行与否无关,与游戏运行的性能有关,平均提升 10% (蚊子腿也是肉啊)
ipwx
2021-03-18 12:55:02 +08:00
python 的线程是真的系统线程,只不过有个 gil 所以不会有多个线程同时执行而已。然而只要是真的线程发生了切换,python 解释器的内部状态同步就要让很多 cpu cache 分支预测之类的黑魔法失效。一个简单例子,cpu 从核内缓存读数据是 1ns 级别的,从主存调数据是 100ns 级别的。如果多线程被分在不同核上,那么一个线程改了一个变量,会导致其他核上的这个变量的核内缓存失效,下一次调度就要重新读内存。。。所以大概这也是为啥有些平台上多线程这么差的原因。
ipwx
2021-03-18 12:56:41 +08:00
有些系统可能会倾向于让同一个进程的不同线程在相同核上执行,那么影响就会小很多。。。有些没这个意识就遭罪了。最怕的就是同一个线程还在不同核上反复横跳,当然一般都不会有智障操作系统这么搞的,除非负载真的很大
ipwx
2021-03-18 12:58:20 +08:00
而且雪上加霜的是,核内缓存 cpu l1-l3 cache 是以 cache line 为单位缓存数据的。我记得 cache line 至少是 64B 。这导致一个变量失效就会让多个变量同时失效。。。()

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/762657

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX