有没有人注意观察过, Python 多进程执行同一程序速度比单进程执行慢很多,原因是什么? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
LeeReamond
V2EX    Python

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

  •  
  •   LeeReamond 2021 年 3 月 18 日 2863 次点击
    这是一个创建于 1842 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    42 条回复    2021-03-18 13:46:01 +08:00
    codehz
        1
    codehz  
       2021 年 3 月 18 日 via Android
    不考虑线程间通讯的成本的吗,只要你需要统一搜集结果(或者线程同步),就会有通讯成本的问题,这个影响是很大的
    除此之外,消费级 cpu 还有超线程的影响
    以及多个核心同时工作导致无法同时达到最大睿频
    或者干脆笔记本撞功耗墙
    laurencedu
        2
    laurencedu  
       2021 年 3 月 18 日
    没有探究过原因,但实际上 python 多线程的效率相比 java 或者 c++是很低的我们团队一般认为 python 的多线程没有效率,不会比单进程快多少。通常如果需要并发执行任务,我们这边都是起多个 python 进程(多个程序)使用不同的参数一起跑。
    love
        3
    love  
       2021 年 3 月 18 日   1
    你都说 GIL 了,这货不就是干这个用的,一个大锁就相当于就是单线程的解释器,搞多线程的假象只是为了 IO 分片,不是是计算分片
    aydd2004
        4
    aydd2004  
       2021 年 3 月 18 日 via iPhone
    @laurencedu 原来不只我这种菜鸡这么干 哈哈哈哈
    ysc3839
        5
    ysc3839  
       2021 年 3 月 18 日 via Android   1
    你的想法是不是:单线程执行的时候只使用了一个核心,耗时 T,多线程使用所有核心,但不同核心之间是不影响的,所以耗时也应该是 T ?
    我估计是睿频的影响,有空我试试用 C++写一个,并且锁定 CPU 频率看看结果如何。
    wzb0909
        6
    wzb0909  
       2021 年 3 月 18 日 via iPhone   4
    我 tm 就不该把楼主从 block 里放出来
    LeeReamond
        7
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android   2
    @wzb0909 谢谢,block 了
    vicalloy
        8
    vicalloy  
       2021 年 3 月 18 日
    先看一下操作系统的资源占用情况,看看每个 CPU 核心的资源占用率。
    LeeReamond
        9
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android   4
    @love
    @laurencedu
    @codehz 感谢各位回复,不过我帖子中讨论的确实是多进程,并且除了说明以外给出了测试代码及执行结果。并不是各位在讨论的所谓线程效率的问题

    我最近确实震惊于程序员群体语文阅读能力之低下,最近几天在 v2 讨论遇到了很多次驴唇不对马嘴的回复,实在不吐不快。
    LeeReamond
        10
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android
    @vicalloy 多进程模式下 16 线程跑 8 进程,其中 8 线程是满载的,剩下占用在 20-60%之间抖动。测试平台 windows,空载状态下运行,我不认为是系统资源不足的影响。
    LeeReamond
        11
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android
    @ysc3839 确实,大佬给出了一个合理的思路。不过如我测试,绝对执行时间增长了三倍,睿频应该差不了这么多吧。
    vipppppp
        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
    LeeReamond
        13
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android
    @vipppppp 感谢,看来确实可能是我之前忽略了睿频的问题,不过大佬你这个结果里多进程是符合期望的,多线程在 gil 下顺序执行,不应该这么慢
    codehz
        14
    codehz  
       2021 年 3 月 18 日 via Android
    @LeeReamond 我特意规避 GIL 和进程创建成本就是防着这一手,结果还是防不胜防啊
    跨越进程的通讯当然也算是线程通讯,毕竟线程是基本执行单位,只是不能使用进程内的机制而已,这个意义说跨越物理 cpu甚至物理机器的通讯也是线程间通讯。
    vipppppp
        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

    反正就是结果差别都很大吧。。。
    no1xsyzy
        16
    no1xsyzy  
       2021 年 3 月 18 日
    @codehz 你这有点牵强了…… 而且通信成本倒不是大问题。
    @vipppppp 可否限定到单一核心后再尝试看下 multi thread ?我觉得有可能是跨核心导致的问题(比如跨 NUMA 节点? GIL 在 CPU 缓存中反复失效?)。
    WinG
        17
    WinG  
       2021 年 3 月 18 日 via Android
    9900k 单核睿频可以跑到 5.1g 全核睿频只有 4.6g
    LeeReamond
        18
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android
    @vipppppp 我无法解释你的跑分结果,虽然这个代码也只是图一乐,并不严谨,但是应该不会影响大方向结论。比如你在超多核的机器上多线程反而特别慢,我觉得有可能是同一段逻辑在不同物理核心上交替运行,期间资源移来移去产生的开销。不过在 12 线程上也这么慢就不合理了,12 线程可不太像是双路 cpu 。。。因为是纯 python 实现,正常多线程的 speedup 就应该是 1.0 左右
    LeeReamond
        19
    LeeReamond  
    OP
       2021 年 3 月 18 日 via Android
    @no1xsyzy 刚才群里跟大佬讨论,大佬说你这个进程间通讯时间都没算,测个屁。我倒只是想得出个大方向结论,没想那么精确,不过我觉得在预激的基础上,进程间通讯的开销应该在微秒级,最慢不会超过几毫秒,这不是影响 4 秒执行时间延长到 12 秒的理由
    vipppppp
        20
    vipppppp  
       2021 年 3 月 18 日
    @no1xsyzy
    是的,如果绑定在同一个核心上,multi thread 的值就很接近 single thread
    vipppppp
        21
    vipppppp  
       2021 年 3 月 18 日
    @LeeReamond
    在其中一台机器上,我测试的时候看了 cpu,多线程指定 2 个 cpu(逻辑)的时候,2 个核心各占 50%。
    如果指定一个个的话,那么多线程就是这个的 100%。
    vipppppp
        22
    vipppppp  
       2021 年 3 月 18 日
    2 个核心各占 50% => 不是完全的 50,一个 50 多,一个 40 多,
    tusj
        23
    tusj  
       2021 年 3 月 18 日
    我记得 python 的多线程是假的
    no1xsyzy
        24
    no1xsyzy  
       2021 年 3 月 18 日
    @LeeReamond 你这边就传个函数名称再来回各传个 int,也是在一块芯片里,能有多少的通讯开支……

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

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

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

    我注意到这个操作可能是有用的起因,Windows 上有一个 CPU Cores 来强制处理游戏的进程核心对应关系,把操作系统其他内容全部丢给一个核心,其余核心全力跑游戏。但我并没有很多实验数据去理解它以何种方式、在何种条件下有何等程度的作用。
    yazoox
        32
    yazoox  
       2021 年 3 月 18 日
    没用过。只能关注学习一波了。
    linw1995
        33
    linw1995  
       2021 年 3 月 18 日
    os 有用 cgroups 对 cpu 使用率作限制吗?或者说,你有排除这类影响因素吗
    systemcall
        34
    systemcall  
       2021 年 3 月 18 日
    @no1xsyzy #31
    没有发现 Windows 的 CPU 调度和游戏是否运行有多大的关系
    如果你的 Windows 系统比你的 CPU 新,一般是会正确处理超线程的,但是一个核心的负载不算大的时候可能会因为节能方面的策略把 2 个线程都用起来。这个你拿 hwinfo 之类的可以准确的看到硬件的比较详细的状态的软件可以看出来。
    blackbbc
        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 跑分结果
    johnsona
        36
    johnsona  
       2021 年 3 月 18 日 via iPhone
    @laurencedu 你们那什么团队试过多线程和单线程在 io 密集场景的对比
    no1xsyzy
        37
    no1xsyzy  
       2021 年 3 月 18 日
    @systemcall 可能有点不清晰
    有一个 Windows 上的第三方软件叫 “CPU Cores”,该软件通过调用系统 API 迫使游戏以外的所有活动分配到单一核心,而为游戏分配其他所有 CPU 。
    与游戏的运行与否无关,与游戏运行的性能有关,平均提升 10% (蚊子腿也是肉啊)
    ipwx
        38
    ipwx  
       2021 年 3 月 18 日 via iPhone
    python 的线程是真的系统线程,只不过有个 gil 所以不会有多个线程同时执行而已。然而只要是真的线程发生了切换,python 解释器的内部状态同步就要让很多 cpu cache 分支预测之类的黑魔法失效。一个简单例子,cpu 从核内缓存读数据是 1ns 级别的,从主存调数据是 100ns 级别的。如果多线程被分在不同核上,那么一个线程改了一个变量,会导致其他核上的这个变量的核内缓存失效,下一次调度就要重新读内存。。。所以大概这也是为啥有些平台上多线程这么差的原因。
    ipwx
        39
    ipwx  
       2021 年 3 月 18 日 via iPhone
    有些系统可能会倾向于让同一个进程的不同线程在相同核上执行,那么影响就会小很多。。。有些没这个意识就遭罪了。最怕的就是同一个线程还在不同核上反复横跳,当然一般都不会有智障操作系统这么搞的,除非负载真的很大
    ipwx
        40
    ipwx  
       2021 年 3 月 18 日 via iPhone
    而且雪上加霜的是,核内缓存 cpu l1-l3 cache 是以 cache line 为单位缓存数据的。我记得 cache line 至少是 64B 。这导致一个变量失效就会让多个变量同时失效。。。()
    binux
        41
    binux  
       2021 年 3 月 18 日 via Android
    进程间通信开销没有那么大
    多核性能并不是单核性能 xN,你去看 CPU benchmark 就能注意到
    你代码统计的是最慢的一个进程用时,试试分别计时再加起来?
    LeeReamond
        42
    LeeReamond  
    OP
       2021 年 3 月 18 日
    @binux 提这个问题主要是我 8 核才提升 2 倍性能,比较诡异所以来问一下,看大家的跑分感觉可能是我硬件问题,不是 python 或者调用方式有问题。物理 8 核心分配 8 线程,资源足够的情况下执行顺序先后完成时间差别不大,属于比较粗略的计算方式,因为不需要进程间通讯。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2785 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 11:08 PVG 19:08 LAX 04:08 JFK 07:08
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86