将一个大对象的方法传递给imap:通过包装方法加速1000倍

ere*_*ner 5 python parallel-processing multiprocessing

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

如果我通过yo.doubleimapmultiprocessing,那么它是非常缓慢的,因为每一个函数调用创建一个副本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
Run Code Online (Sandbox Code Playgroud)

输出:

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]
Run Code Online (Sandbox Code Playgroud)

...

但是,如果我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
Run Code Online (Sandbox Code Playgroud)

输出:

0it [00:00, ?it/s]
1000it [00:00, 14919.34it/s]
Run Code Online (Sandbox Code Playgroud)

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

我使用Python 3.6.6.

Dar*_*aut 3

关于复制,你说得对。yo.double是一个“绑定方法”,绑定到你的大对象。当您将其传递到池方法时,它将用它来pickle整个实例,将其发送到子进程并在那里取消pickle。对于子进程处理的可迭代的每个块都会发生这种情况。默认值chunksizeinpool.imap1,因此对于可迭代中的每个已处理项,您都会遇到此通信开销。

\n\n

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

\n\n
\n\n
\n

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

\n
\n\n

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

\n\n

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

\n\n

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

\n