【问题标题】:Passing a method of a big object to imap: 1000-fold speed-up by wrapping the method将大对象的方法传递给imap:通过包装方法实现1000倍加速
【发布时间】:2026-02-09 21:20:03
【问题描述】:

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

如果我将yo.double 传递给multiprocessingimap,那么它会非常慢,因为我认为每个函数调用都会创建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]

...

但是,如果我用函数 double_wrap 包装 yo.double 并将其传递给 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。

【问题讨论】:

  • 我的回答有什么不清楚的地方吗?
  • @Darkonaut 我只是不明白为什么制作功能模块级别会阻止对象的复制。毕竟,该函数需要有一个指向yo 对象本身的指针——这应该要求所有进程复制yo,因为它们不能共享内存。
  • 我将我对上述评论的回复重新定位到我的答案中。

标签: python parallel-processing multiprocessing


【解决方案1】:

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

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


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

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

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

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

【讨论】: