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-03-18 06:58:35 +08:00 · 2108 次点击
    这是一个创建于 1381 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

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

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

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

    我注意到这个操作可能是有用的起因,Windows 上有一个 CPU Cores 来强制处理游戏的进程核心对应关系,把操作系统其他内容全部丢给一个核心,其余核心全力跑游戏。但我并没有很多实验数据去理解它以何种方式、在何种条件下有何等程度的作用。
    yazoox
        32
    yazoox  
       2021-03-18 11:48:28 +08:00
    没用过。只能关注学习一波了。
    linw1995
        33
    linw1995  
       2021-03-18 12:28:40 +08:00
    os 有用 cgroups 对 cpu 使用率作限制吗?或者说,你有排除这类影响因素吗
    systemcall
        34
    systemcall  
       2021-03-18 12:34:33 +08:00
    @no1xsyzy #31
    没有发现 Windows 的 CPU 调度和游戏是否运行有多大的关系
    如果你的 Windows 系统比你的 CPU 新,一般是会正确处理超线程的,但是一个核心的负载不算大的时候可能会因为节能方面的策略把 2 个线程都用起来。这个你拿 hwinfo 之类的可以准确的看到硬件的比较详细的状态的软件可以看出来。
    blackbbc
        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 跑分结果
    johnsona
        36
    johnsona  
       2021-03-18 12:45:47 +08:00 via iPhone
    @laurencedu 你们那什么团队试过多线程和单线程在 io 密集场景的对比
    no1xsyzy
        37
    no1xsyzy  
       2021-03-18 12:50:52 +08:00
    @systemcall 可能有点不清晰
    有一个 Windows 上的第三方软件叫 “CPU Cores”,该软件通过调用系统 API 迫使游戏以外的所有活动分配到单一核心,而为游戏分配其他所有 CPU 。
    与游戏的运行与否无关,与游戏运行的性能有关,平均提升 10% (蚊子腿也是肉啊)
    ipwx
        38
    ipwx  
       2021-03-18 12:55:02 +08:00 via iPhone
    python 的线程是真的系统线程,只不过有个 gil 所以不会有多个线程同时执行而已。然而只要是真的线程发生了切换,python 解释器的内部状态同步就要让很多 cpu cache 分支预测之类的黑魔法失效。一个简单例子,cpu 从核内缓存读数据是 1ns 级别的,从主存调数据是 100ns 级别的。如果多线程被分在不同核上,那么一个线程改了一个变量,会导致其他核上的这个变量的核内缓存失效,下一次调度就要重新读内存。。。所以大概这也是为啥有些平台上多线程这么差的原因。
    ipwx
        39
    ipwx  
       2021-03-18 12:56:41 +08:00 via iPhone
    有些系统可能会倾向于让同一个进程的不同线程在相同核上执行,那么影响就会小很多。。。有些没这个意识就遭罪了。最怕的就是同一个线程还在不同核上反复横跳,当然一般都不会有智障操作系统这么搞的,除非负载真的很大
    ipwx
        40
    ipwx  
       2021-03-18 12:58:20 +08:00 via iPhone
    而且雪上加霜的是,核内缓存 cpu l1-l3 cache 是以 cache line 为单位缓存数据的。我记得 cache line 至少是 64B 。这导致一个变量失效就会让多个变量同时失效。。。()
    binux
        41
    binux  
       2021-03-18 13:02:59 +08:00 via Android
    进程间通信开销没有那么大
    多核性能并不是单核性能 xN,你去看 CPU benchmark 就能注意到
    你代码统计的是最慢的一个进程用时,试试分别计时再加起来?
    LeeReamond
        42
    LeeReamond  
    OP
       2021-03-18 13:46:01 +08:00
    @binux 提这个问题主要是我 8 核才提升 2 倍性能,比较诡异所以来问一下,看大家的跑分感觉可能是我硬件问题,不是 python 或者调用方式有问题。物理 8 核心分配 8 线程,资源足够的情况下执行顺序先后完成时间差别不大,属于比较粗略的计算方式,因为不需要进程间通讯。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2065 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 00:38 · PVG 08:38 · LAX 16:38 · JFK 19:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.