使用multiprocessing.Manager.list而不是真实列表会使计算花费很长时间

时间:2012-10-29 12:36:54

标签: python multiprocessing

我想从这个例子开始尝试使用multiprocessing的不同方法:

$ cat multi_bad.py 
import multiprocessing as mp
from time import sleep
from random import randint

def f(l, t):
#   sleep(30)
    return sum(x < t for x in l)

if __name__ == '__main__':
    l = [randint(1, 1000) for _ in range(25000)]
    t = [randint(1, 1000) for _ in range(4)]
#   sleep(15)
    pool = mp.Pool(processes=4)
    result = pool.starmap_async(f, [(l, x) for x in t])
    print(result.get())

此处,l是一个列表,在生成4个进程时会被复制4次。为避免这种情况,文档页面提供了使用multiprocessing.Manager创建的队列,共享阵列或代理对象。对于最后一个,我更改了l

的定义
$ diff multi_bad.py multi_good.py 
10c10,11
<     l = [randint(1, 1000) for _ in range(25000)]
---
>     man = mp.Manager()
>     l = man.list([randint(1, 1000) for _ in range(25000)])

结果看起来仍然正确,但执行时间大幅增加,我认为我做错了什么:

$ time python multi_bad.py 
[17867, 11103, 2021, 17918]

real    0m0.247s
user    0m0.183s
sys 0m0.010s

$ time python multi_good.py 
[3609, 20277, 7799, 24262]

real    0m15.108s
user    0m28.092s
sys 0m6.320s

文档确实说这种方式比共享数组慢,但这只是错误的。我也不确定如何对此进行分析以获得有关正在发生的事情的更多信息。我错过了什么吗?

P.S。使用共享阵列时,我的时间会低于0.25秒。

P.P.S。这是在Linux和Python 3.3上。

2 个答案:

答案 0 :(得分:8)

当子进程os.fork时,Linux使用copy-on-write。为了证明:

import multiprocessing as mp
import numpy as np
import logging
import os

logger = mp.log_to_stderr(logging.WARNING)

def free_memory():
    total = 0
    with open('/proc/meminfo', 'r') as f:
        for line in f:
            line = line.strip()
            if any(line.startswith(field) for field in ('MemFree', 'Buffers', 'Cached')):
                field, amount, unit = line.split()
                amount = int(amount)
                if unit != 'kB':
                    raise ValueError(
                        'Unknown unit {u!r} in /proc/meminfo'.format(u = unit))
                total += amount
    return total

def worker(i):
    x = data[i,:].sum()    # Exercise access to data
    logger.warn('Free memory: {m}'.format(m = free_memory()))

def main():
    procs = [mp.Process(target = worker, args = (i, )) for i in range(4)]
    for proc in procs:
        proc.start()
    for proc in procs:
        proc.join()

logger.warn('Initial free: {m}'.format(m = free_memory()))
N = 15000
data = np.ones((N,N))
logger.warn('After allocating data: {m}'.format(m = free_memory()))

if __name__ == '__main__':
    main()

产生了

[WARNING/MainProcess] Initial free: 2522340
[WARNING/MainProcess] After allocating data: 763248
[WARNING/Process-1] Free memory: 760852
[WARNING/Process-2] Free memory: 757652
[WARNING/Process-3] Free memory: 757264
[WARNING/Process-4] Free memory: 756760

这表明最初有大约2.5GB的可用内存。 分配一个15000x15000的float64 s数组后,有763248 KB可用空间。这大概有意义,因为15000 ** 2 * 8字节= 1.8GB,内存下降,2.5GB - 0.763248GB也大约是1.8GB。

现在每个进程产生后,再次报告可用内存为~750MB。可用内存没有明显减少,因此我得出结论,系统必须使用copy-on-write。

结论:如果您不需要修改数据,那么在__main__模块的全局级别定义数据是一种方便且(至少在Linux上)内存友好的方式,可以在子进程之间共享它。 / p>

答案 1 :(得分:3)

这是可以预料到的,因为访问共享对象意味着必须通过某种信号/系统调用来发送它请求执行它并以相同的方式返回结果。

基本上你应该尽量避免共享内存。这会导致更多的可调试代码(因为你的并发性要少得多),而且速度更快。

只有在真正需要的情况下才能使用共享内存(例如,共享千兆字节的数据,以便复制它需要太多RAM或者进程应该能够通过这个共享内存进行交互)。

另一方面,可能使用Manager比共享阵列慢得多,因为Manager必须能够处理任何PyObject *,因此必须pickle / unpickle等,而阵列可以避免大量的开销。 / p>

从多处理的文档:

  

管理员提供了一种创建可在其间共享的数据的方法   不同的过程。管理器对象控制服务器进程   管理共享对象。其他进程可以访问共享对象   通过使用代理。

因此,使用Manager意味着生成一个仅用于处理共享内存的新进程,这可能是为什么它需要更多时间。

如果您尝试分析代理的速度,它的很多比非共享列表慢:

>>> import timeit
>>> import multiprocessing as mp
>>> man = mp.Manager()
>>> L = man.list(range(25000))
>>> timeit.timeit('L[0]', 'from __main__ import L')
50.490395069122314
>>> L = list(range(25000))
>>> timeit.timeit('L[0]', 'from __main__ import L')
0.03588080406188965
>>> 50.490395069122314 / _
1407.1701119638526

虽然Array并没有那么慢:

>>> L = mp.Array('i', range(25000))
>>> timeit.timeit('L[0]', 'from __main__ import L')
0.6133401393890381
>>> 0.6133401393890381 / 0.03588080406188965
17.09382371507359

由于非常基本的操作很慢,并且认为没有太多希望加速它们,这意味着如果你必须共享一个大的数据列表并希望快速访问它,那么你应该使用{{ 1}}。

可能会加速一些事情的是一次访问多个元素(例如,获取切片而不是单个元素),但是根据您想要做的事情,这可能是也可能是不可能的。