Python multiprocessing模块开销异常:子进程耗时超父进程10倍求助
你观察到的现象核心原因并非子进程内存限制,而是进程间传递大numpy数组时的序列化/反序列化开销——这个开销在大数组场景下会彻底盖过计算本身的耗时,导致子进程总耗时是父进程的数倍甚至十几倍。
1. 父进程vs子进程的本质差异:数据传递的隐形开销
当你直接在父进程调用numpy.mean(x)时,数组x已经在当前进程的内存空间里,计算只需直接操作内存,没有额外开销。
但使用multiprocessing.Process传递x作为参数时,背后发生了这些隐形操作:
- 父进程需要把整个numpy数组通过pickle序列化(转换成字节流)
- 把字节流通过进程间通信(IPC)传递给子进程
- 子进程再把字节流反序列化还原成numpy数组
- 之后才会执行
numpy.mean计算
对于小数组,序列化/反序列化的时间可以忽略,但当数组达到几百MB甚至2GB时,这个过程的耗时会远远超过计算均值的时间——比如计算2GB数组的均值可能只需要几十毫秒,但序列化整个数组可能需要几百毫秒到几秒,这就直接导致子进程总耗时是父进程的10倍左右。
你的do_nothing测试也能佐证这点:空操作的子进程耗时同样会随数组增大而飙升,说明核心开销根本不在计算,而在数据传递环节。
2. 为什么不是内存限制?
内存限制通常会表现为进程卡顿、OOM(内存不足)报错,或者计算时频繁触发磁盘交换(swap)导致变慢,但你的测试中父进程直接计算速度正常,说明内存足够支撑计算。子进程的慢是「前置数据准备时间太长」,而非计算过程受内存限制。
3. 解决方案:避免大数组的进程间复制
要解决这个问题,核心是让子进程直接访问父进程的数组内存,无需序列化传递,具体有两种常见方案:
方案一:利用Unix系统的fork特性(Linux/macOS适用)
Unix下multiprocessing默认使用fork方式创建子进程,子进程会直接继承父进程的内存空间。你可以把数组定义为全局变量,或者在创建进程前初始化数组,子进程直接读取,不需要作为参数传递:
import numpy, multiprocessing, pandas # 全局变量存储数组,子进程直接访问 global_x = None def do_nothing(q): q.put(global_x[-1]) def my_mean(q): q.put(numpy.mean(global_x)) def test_mp(f): q = multiprocessing.Queue() p = multiprocessing.Process(target=f, args=(q,)) p.start() p.join() s = q.get() return s ndata = 2**numpy.arange(10,29,2) tr1,tr2,tr3 = [[],[],[]] for n in ndata: global global_x global_x = numpy.random.rand(n) tresults = %timeit -n 1 -r 5 -o -q test_mp(do_nothing) tr1.append(tresults) tresults = %timeit -n 1 -r 5 -o -q test_mp(my_mean) tr2.append(tresults) tresults = %timeit -n 1 -r 5 -o -q numpy.mean(global_x) tr3.append(tresults) # 后续处理不变...
这种方式下,子进程不需要复制数组,直接复用父进程的内存页(写时复制,这里是只读操作,不会触发复制),序列化开销几乎为零,子进程耗时会和父进程接近。
方案二:使用共享内存(跨平台适用)
如果需要兼容Windows(Windows下multiprocessing默认用spawn,无法继承内存),可以用multiprocessing.Array创建共享内存数组,再用numpy包装成数组对象:
import numpy, multiprocessing, pandas def do_nothing(arr_shape, q): # 从共享内存还原numpy数组 x = numpy.frombuffer(arr_shape[0], dtype=numpy.float64).reshape(arr_shape[1]) q.put(x[-1]) def my_mean(arr_shape, q): x = numpy.frombuffer(arr_shape[0], dtype=numpy.float64).reshape(arr_shape[1]) q.put(numpy.mean(x)) def test_mp(f, x): # 创建共享内存数组,复制数据进去 shared_arr = multiprocessing.Array('d', x.size, lock=False) shared_np = numpy.frombuffer(shared_arr, dtype=numpy.float64) shared_np[:] = x.flatten() q = multiprocessing.Queue() p = multiprocessing.Process(target=f, args=((shared_arr, x.shape), q)) p.start() p.join() s = q.get() return s # 后续测试逻辑不变...
共享内存方式下,数据只需要复制一次到共享内存,子进程直接读取,避免了pickle序列化的巨大开销。
4. 验证你的观察
你可以单独测试序列化大数组的耗时,直观感受开销大小:
import pickle import numpy as np x = np.random.rand(2**28) # 约2GB %timeit pickle.dumps(x) %timeit pickle.loads(pickle.dumps(x))
这个耗时应该和你子进程测试中的额外耗时基本一致。
内容的提问来源于stack exchange,提问作者Donna

