将大对象的方法传递给imap:通过包装该方法将速度提高1000倍

时间:2018-10-16 13:02:02

标签: python parallel-processing multiprocessing

假设yo = Yo()是一个带有方法double的大对象,该方法返回其参数乘以2

如果我将yo.double传递给imap的{​​{1}},那么它会非常慢,因为我认为每个函数调用都会创建multiprocessing的副本。

也就是说,这非常慢:

yo

输出:

from tqdm import tqdm
from multiprocessing import Pool
import numpy as np


class Yo:
    def __init__(self):
        self.a = np.random.random((10000000, 10))

    def double(self, x):
        return 2 * x

yo = Yo()    

with Pool(4) as p:
    for _ in tqdm(p.imap(yo.double, np.arange(1000))):
        pass

...

但是,如果我用函数0it [00:00, ?it/s] 1it [00:06, 6.54s/it] 2it [00:11, 6.17s/it] 3it [00:16, 5.60s/it] 4it [00:20, 5.13s/it] 包装yo.double并将其传递给double_wrap,那么它实际上是瞬时的。

imap

输出:

def double_wrap(x):
    return yo.double(x)

with Pool(4) as p:
    for _ in tqdm(p.imap(double_wrap, np.arange(1000))):
        pass

包装函数如何以及为何改变行为?

我使用Python 3.6.6。

1 个答案:

答案 0 :(得分:1)

您对复制是正确的。 yo.double是绑定到您的大对象的“绑定方法”。当您将其传递到池方法中时,它将使用它来腌制整个实例,将其发送到子进程并在那里进行修补。对于子进程所处理的每个可迭代块,都会发生这种情况。 chunksizepool.imap的默认值为1,因此您要为Iterable中的每个已处理项目节省通信开销。

相反,当您传递double_wrap时,您只是在传递模块级函数。实际上,只有它的名称会被腌制,并且子进程将从__main__导入函数。由于您显然是在支持分叉的操作系统上,因此您的double_wrap函数将有权访问yo的分叉的Yo实例。在这种情况下,您的大对象将不会被序列化(插入),因此与其他方法相比,通信开销很小。


  

@Darkonaut我只是不明白为什么将功能模块设置为水平会阻止对象的复制。毕竟,该函数需要有一个指向yo对象本身的指针-这应该要求所有进程都复制yo,因为它们无法共享内存。

在子进程中运行的函数将自动找到对全局yo的引用,因为您的操作系统(OS)正在使用fork创建子进程。分叉会导致整个父进程的克隆,只要父进程和子进程都没有更改特定的对象,那么两者都将在相同的内存位置看到相同的对象。

仅当父级或子级更改对象上的某些内容时,对象的对象才会在子进程中被复制。这就是所谓的“写时复制”,发生在操作系统级别,而您没有在Python中注意到它。您的代码在Windows上无法使用,Windows使用“ spawn”作为新进程的启动方法。

现在,我在上面写“对象被复制”的地方进行了简化,因为操作系统所操作的单元是一个“页面”(最常见的大小是4KB)。这个答案here是扩大您的理解的很好的后续阅读。