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
这样的函数时,f
(def
-ed)知道它的限定名称(在交互式解释器或程序的主模块中,它是 __main__.f
),因此远程端可以通过执行 from __main__ import f
的等效操作在本地重新创建它。但是 partial
不知道它的名字;当然,您将它分配给 g
,但是 pickle
和 partial
本身都不知道它可以使用限定名称 __main__.g
;它可以命名为 foo.fred
或一百万个其他名称。所以它必须 pickle
完全从头开始重新创建它所必需的信息。它也是 pickle
-ing 每次调用(不仅仅是每个工作人员一次),因为它不知道可调用对象在工作项之间的父级中没有变化,并且它总是试图确保它发送最新的状态。
您还有其他问题(仅在 partial
情况下定时创建 list
以及调用 partial
包装函数与直接调用函数的开销较小),但是相对于 partial
正在添加的每次调用开销 pickling 和 unpickling 来说,这些都是笨拙的变化( list
的初始创建增加了 [=50= 的一半以下的一次性开销]each pickle/unpickle 周期成本;通过 partial
调用的开销小于一微秒)。
考虑以下函数:
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
这样的函数时,f
(def
-ed)知道它的限定名称(在交互式解释器或程序的主模块中,它是 __main__.f
),因此远程端可以通过执行 from __main__ import f
的等效操作在本地重新创建它。但是 partial
不知道它的名字;当然,您将它分配给 g
,但是 pickle
和 partial
本身都不知道它可以使用限定名称 __main__.g
;它可以命名为 foo.fred
或一百万个其他名称。所以它必须 pickle
完全从头开始重新创建它所必需的信息。它也是 pickle
-ing 每次调用(不仅仅是每个工作人员一次),因为它不知道可调用对象在工作项之间的父级中没有变化,并且它总是试图确保它发送最新的状态。
您还有其他问题(仅在 partial
情况下定时创建 list
以及调用 partial
包装函数与直接调用函数的开销较小),但是相对于 partial
正在添加的每次调用开销 pickling 和 unpickling 来说,这些都是笨拙的变化( list
的初始创建增加了 [=50= 的一半以下的一次性开销]each pickle/unpickle 周期成本;通过 partial
调用的开销小于一微秒)。