首页 > 解决方案 > 将大对象的方法传递给imap:通过包装方法实现1000倍加速

问题描述

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

如果我传递yo.doubleimapof 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

输出:

0it [00:00, ?it/s]
1000it [00:00, 14919.34it/s]

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

我使用 Python 3.6.6。

标签: pythonparallel-processingmultiprocessing

解决方案


你对复制的看法是对的。yo.double是一个“绑定方法”,绑定到你的大对象。当您将它传递给池方法时,它将用它腌制整个实例,将其发送到子进程并在那里取消腌制。这发生在子进程工作的每个可迭代块上。chunksizein的默认值为pool.imap1,因此您正在为可迭代中的每个已处理项目达到此通信开销。

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


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

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

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

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


推荐阅读