有没有人注意观察过, 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 倍,并不能提升很多,以前没有细究,现在觉得不太对

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

我最近确实震惊于程序员群体语文阅读能力之低下,最近几天在 v2 讨论遇到了很多次驴唇不对马嘴的回复,实在不吐不快。
LeeReamond
2021-03-18 09:10:33 +08:00
@vicalloy 多进程模式下 16 线程跑 8 进程,其中 8 线程是满载的,剩下占用在 20-60%之间抖动。测试平台 windows,空载状态下运行,我不认为是系统资源不足的影响。
LeeReamond
2021-03-18 09:11:50 +08:00
@ysc3839 确实,大佬给出了一个合理的思路。不过如我测试,绝对执行时间增长了三倍,睿频应该差不了这么多吧。
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
2021-03-18 09:29:41 +08:00
@vipppppp 感谢,看来确实可能是我之前忽略了睿频的问题,不过大佬你这个结果里多进程是符合期望的,多线程在 gil 下顺序执行,不应该这么慢
codehz
2021-03-18 09:50:04 +08:00
@LeeReamond 我特意规避 GIL 和进程创建成本就是防着这一手,结果还是防不胜防啊
跨越进程的通讯当然也算是线程通讯,毕竟线程是基本执行单位,只是不能使用进程内的机制而已,这个意义说跨越物理 cpu,甚至物理机器的通讯也是线程间通讯。
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
2021-03-18 10:08:02 +08:00
@codehz 你这有点牵强了…… 而且通信成本倒不是大问题。
@vipppppp 可否限定到单一核心后再尝试看下 multi thread ?我觉得有可能是跨核心导致的问题(比如跨 NUMA 节点? GIL 在 CPU 缓存中反复失效?)。
WinG
2021-03-18 10:12:48 +08:00
9900k 单核睿频可以跑到 5.1g 全核睿频只有 4.6g
LeeReamond
2021-03-18 10:23:23 +08:00
@vipppppp 我无法解释你的跑分结果,虽然这个代码也只是图一乐,并不严谨,但是应该不会影响大方向结论。比如你在超多核的机器上多线程反而特别慢,我觉得有可能是同一段逻辑在不同物理核心上交替运行,期间资源移来移去产生的开销。不过在 12 线程上也这么慢就不合理了,12 线程可不太像是双路 cpu 。。。因为是纯 python 实现,正常多线程的 speedup 就应该是 1.0 左右
LeeReamond
2021-03-18 10:25:40 +08:00
@no1xsyzy 刚才群里跟大佬讨论,大佬说你这个进程间通讯时间都没算,测个屁。我倒只是想得出个大方向结论,没想那么精确,不过我觉得在预激的基础上,进程间通讯的开销应该在微秒级,最慢不会超过几毫秒,这不是影响 4 秒执行时间延长到 12 秒的理由
vipppppp
2021-03-18 10:27:52 +08:00
@no1xsyzy
是的,如果绑定在同一个核心上,multi thread 的值就很接近 single thread

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

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

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

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

© 2021 V2EX