14

考虑以下函数:

def f(x, dummy=list(range(10000000))):
    return x

如果我使用multiprocessing.Pool.imap,我会得到以下时间:

import time
import os
from multiprocessing import Pool

def f(x, dummy=list(range(10000000))):
    return x

start = time.time()
pool = Pool(2)
for x in pool.imap(f, range(10)):
    print("parent process, x=%s, elapsed=%s" % (x, int(time.time() - start)))

parent process, x=0, elapsed=0
parent process, x=1, elapsed=0
parent process, x=2, elapsed=0
parent process, x=3, elapsed=0
parent process, x=4, elapsed=0
parent process, x=5, elapsed=0
parent process, x=6, elapsed=0
parent process, x=7, elapsed=0
parent process, x=8, elapsed=0
parent process, x=9, elapsed=0

现在,如果我使用functools.partial而不是使用默认值:

import time
import os
from multiprocessing import Pool
from functools import partial

def f(x, dummy):
    return x

start = time.time()
g = partial(f, dummy=list(range(10000000)))
pool = Pool(2)
for x in pool.imap(g, range(10)):
    print("parent process, x=%s, elapsed=%s" % (x, int(time.time() - start)))

parent process, x=0, elapsed=1
parent process, x=1, elapsed=2
parent process, x=2, elapsed=5
parent process, x=3, elapsed=7
parent process, x=4, elapsed=8
parent process, x=5, elapsed=9
parent process, x=6, elapsed=10
parent process, x=7, elapsed=10
parent process, x=8, elapsed=11
parent process, x=9, elapsed=11

为什么版本使用functools.partial这么慢?

4

1 回答 1

15

使用multiprocessing需要向工作进程发送有关要运行的函数的信息,而不仅仅是要传递的参数。该信息是通过在主进程中提取该信息,将其发送到工作进程并在那里取消提取来传输的。

这导致了主要问题:

用默认参数腌制一个函数很便宜;它只腌制函数的名称(加上让 Python 知道它是一个函数的信息);工作进程只是查找名称的本地副本。他们已经有一个命名函数f要查找,因此传递它几乎不需要任何成本。

但是腌制一个partial函数涉及腌制底层函数(便宜)和所有默认参数(默认参数为 10M 长时很昂贵list。因此,每次在partial案例中调度任务时,它都会对绑定的参数进行腌制,将其发送到工作进程,工作进程取消腌制,然后最终完成“真正的”工作。在我的机器上,pickle 的大小大约为 50 MB,这是一个巨大的开销;在我机器上的快速计时测试中,酸洗和解酸 1000 万长list0时间大约需要 620 毫秒(这忽略了实际传输 50 MB 数据的开销)。

partials不得不这样腌,因为他们不知道自己的名字;当酸洗一个函数时ff(被def-ed)知道它的限定名称(在交互式解释器或程序的主模块中,它是__main__.f),因此远程端可以通过执行等效的from __main__ import f. 但partial不知道它的名字;当然,您将它分配给g,但它本身pickle也不partial知道它可以使用限定名称__main__.g; 它可以被命名foo.fred或一百万个其他东西。所以它必须有pickle必要的信息才能完全从头开始重新创建它。这也是pickle- 每次调用(不仅仅是每个工作人员一次),因为它不知道可调用对象在工作项之间的父级中没有改变,并且它总是试图确保它发送最新状态。

您还有其他问题(list在这种情况下创建唯一的时间partial以及调用包装函数与直接调用函数的小开销partial),但这些是相对于每次调用开销酸洗和解酸partial添加的小改动(的初始创建list是增加一次性开销,略低于每个泡菜/解泡周期成本的一半;通过调用的开销partial不到一微秒)。

于 2016-01-28T13:14:12.867 回答