Python 多处理 - 为什么使用 functools.partial 比默认参数慢?

Python multiprocessing - Why is using functools.partial slower than default arguments?

考虑以下函数:

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的版本这么慢?

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

这导致主要问题:

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

但是 pickling 一个 partial 函数涉及 pickling 底层函数(便宜)和 所有 默认参数(昂贵 当默认参数是 10M 长时 list)。因此,每次在 partial 情况下分派任务时,它都会对绑定参数进行 pickle,将其发送到工作进程,工作进程取消 pickle,然后最终执行 "real" 工作。在我的机器上,pickle 的大小大约为 50 MB,这是一个巨大的开销;在我机器上的快速计时测试中,pickling 和 unpickling 1000 万长 list of 0 大约需要 620 毫秒(这忽略了实际传输 50 MB 数据的开销)。

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

您还有其他问题(仅在 partial 情况下定时创建 list 以及调用 partial 包装函数与直接调用函数的开销较小),但是相对于 partial 正在添加的每次调用开销 pickling 和 unpickling 来说,这些都是笨拙的变化( list 的初始创建增加了 [=50= 的一半以下的一次性开销]each pickle/unpickle 周期成本;通过 partial 调用的开销小于一微秒)。