使用multiprocessing.Manager.list而不是真实列表会使计算耗时

发布于 2021-01-29 18:26:33

我想尝试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

文档确实说这种方法比共享数组要慢,但这感觉很不对。我也不确定如何配置此文件,以获取有关正在发生的事情的更多信息。我想念什么吗?

PS与共享数组,我得到的时间低于0.25s。

PPS这是在Linux和Python 3.3上。

关注者
0
被浏览
41
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    ed子进程时,Linux使用复制os.fork。展示:

    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,有763248 KB的可用空间。因为15000 ** 2 * 8个字节= 1.8GB,而内存的下降2.5GB-0.763248GB也大约是1.8GB,所以这大概是有道理的。

    现在,在生成每个进程之后,再次报告可用内存为〜750MB。可用内存没有明显减少,因此我得出结论,系统必须使用写时复制。

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



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看