python - 将大对象的方法传递给imap:通过包装方法实现1000倍加速
问题描述
假设yo = Yo()
是一个带有方法的大对象,该方法double
返回其参数乘以2
。
如果我传递yo.double
给imap
of 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。
解决方案
你对复制的看法是对的。yo.double
是一个“绑定方法”,绑定到你的大对象。当您将它传递给池方法时,它将用它腌制整个实例,将其发送到子进程并在那里取消腌制。这发生在子进程工作的每个可迭代块上。chunksize
in的默认值为pool.imap
1,因此您正在为可迭代中的每个已处理项目达到此通信开销。
相反,当您传递 时double_wrap
,您只是传递了一个模块级函数。只有它的名称实际上会被腌制,并且子进程将从__main__
. 由于您显然在支持分叉的操作系统上,因此您的函数double_wrap
将可以访问. 在这种情况下,您的大对象不会被序列化(腌制),因此与其他方法相比,通信开销很小。yo
Yo
@Darkonaut我只是不明白为什么制作功能模块级别会阻止对象的复制。毕竟,函数需要有一个指向 yo 对象本身的指针——这应该要求所有进程复制 yo,因为它们不能共享内存。
在子进程中运行的函数将自动找到对全局的引用yo
,因为您的操作系统(OS)正在使用 fork 创建子进程。分叉会导致整个父进程的克隆,只要父进程和子进程都没有更改特定对象,两者都会在相同的内存位置看到相同的对象。
仅当父或子更改对象上的某些内容时,才会将对象复制到子进程中。这称为“写时复制”,并且发生在操作系统级别,而您在 Python 中没有注意到它。您的代码无法在 Windows 上运行,它使用“spawn”作为新进程的启动方法。
现在我在上面写“对象被复制”的地方稍微简化了一点,因为操作系统操作的单元是一个“页面”(最常见的是大小为 4KB)。这里的答案将是一个很好的后续阅读,可以扩大您的理解。
推荐阅读
- google-people-api - 谷歌:联系人搜索[方法:people.searchDirectoryPeople]
- c# - 如何解决锁上死锁的 SqlException 通信缓冲资源
- kafka-consumer-api - 如何使用每个主题/分区的背压来使用 Kafka 事件
- reactjs - @apply 在 tailwindcss 中不工作 | 全局 css 文件中的 Next.js
- python - 如何使用 discord.py 创建经济机器人?
- gwt - GWT 内容未在 centOS 7.5 上显示
- java - server.onDispose().block() 是什么意思?
- flutter - Flutter:无法在 Flutter 应用中加载 admob 广告
- typescript - Ramda TypeScript 管道“预期 0 个参数,但得到 1 个。TS2554”没有选择重载
- sql - 如何加快比较子字符串的查询执行速度