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

usu*_* me 11 python python-3.x functools python-multiprocessing

考虑以下功能:

def f(x, dummy=list(range(10000000))):
    return x
Run Code Online (Sandbox Code Playgroud)

如果我使用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
Run Code Online (Sandbox Code Playgroud)

现在,如果我使用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
Run Code Online (Sandbox Code Playgroud)

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

Sha*_*ger 11

使用multiprocessing需要发送有关要运行的函数的工作进程信息,而不仅仅是要传递的参数.通过在主进程中挑选该信息,将其发送到工作进程并在那里进行解压缩来传输该信息.

这导致了主要问题:

使用默认参数挑选函数很便宜 ; 它只腌制函数的名称(加上信息让Python知道它是一个函数); worker进程只查找名称的本地副本.他们已经有了一个f可以找到的命名函数,因此传递它几乎没有任何成本.

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

partial必须以这种方式腌制,因为他们不知道自己的名字; 当腌制函数时f,f(defis -ed)知道它的限定名称(在交互式解释器中或从程序的主模块中,它是__main__.f),因此远程端可以通过执行等效的方式在本地重新创建它from __main__ import f.但是partial不知道它的名字; 当然,你已经分配了它g,但是pickle它们partial本身都不知道它是否具有合格的名称__main__.g; 它可以被命名foo.fred或者其他一百万个.所以它pickle必须完全从头开始重新创建它所需的信息.它也pickle适用于每个调用(不仅仅是每个工作一次),因为它不知道可调用项在工作项之间的父项中没有变化,并且它总是试图确保它发送最新状态.

你有其他的问题(list在这种partial情况下只创建时间和调用partial包装函数与直接调用函数的次要开销),但是那些是相对于每次调用的开销酸洗和取消partial添加的笨蛋更改(最初的创建是在每个 pickle/unpickle循环成本的list一半以下添加一次性开销;通过它调用的开销小于一微秒).partial