【问题标题】:Python multiprocessing - Why is using functools.partial slower than default arguments?Python 多处理 - 为什么使用 functools.partial 比默认参数慢?
【发布时间】:2016-01-28 12:54:07
【问题描述】:

考虑以下函数:

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

【问题讨论】:

  • 你为什么使用list(range(...))? AFAIK 你的代码在没有调用list 的情况下会做同样的事情,除了ShadowRanger 解释的问题不会发生并且酸洗的开销会小得多小。
  • 旁注:使用 lists(或任何其他可变类型)作为默认(或 partial 绑定)参数是危险的,因为 same list在函数的所有默认调用之间共享,而不是每次调用的新副本;通常,您需要新鲜的副本。
  • 顺便说一句,使用可变对象作为默认值通常是个坏主意,因为如果你在函数中修改它,每次后续调用函数都会看到变化
  • @Bakuriu:我认为这只是一个演示差异的最小示例,而不是真实代码。值得赞赏;得到某人的项目的巨大转储并且没有迹象表明他们试图解决这个问题是皇家 PITA。

标签: python python-3.x python-multiprocessing functools


【解决方案1】:

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

这导致了主要问题:

使用默认参数挑选函数很便宜;它只腌制函数的名称(加上让 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,但picklepartial 本身都不知道它具有限定名称__main__.g;它可以命名为foo.fred 或其他一百万个东西。所以它必须pickle 提供完全从头开始重新创建它所需的信息。每次调用也是pickle-ing(不仅仅是每个工作人员一次),因为它不知道可调用对象在工作项之间的父级中没有改变,并且它总是试图确保它发送最新状态。

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

【讨论】:

  • 1) 如果默认参数dummy没有被pickle,那么它是如何发送给worker的呢?它不是全局变量,是吗? 2) 使用partial,每个函数调用都很昂贵。这是否意味着g 会为每个函数调用(重新)腌制?
  • @usualme: #1:在 Linux 上,worker 是从父级派生的,所以他们已经在自己的内存空间中拥有自己的函数副本(它是写时复制,所以他们实际上可能会与父级共享页面一段时间)。而且他们的副本已经初始化了相同的默认参数,因此当他们通过限定名称查找相同的函数时,它已经设置好了。在 Windows 上,Python 通过运行 __main__ 来模拟 fork 而不运行它,就好像它作为主模块运行一样;如果函数是在__main__ 中导入的,则制作列表的成本是每个工人支付一次,而不是任务。
  • @usualme: #2: 是的,Pool 是通用的,不能保证工作进程不会死亡和被替换,启动和接收工作进程结果的进程不会改变传递给imap 的可调用对象,任何给定的工作人员甚至已经收到工作,或者使用不同可调用对象的其他任务可能不会被穿插等等。因此,可调用对象和参数都被序列化以在每个单独的任务上调度,不只是每个工人一次。通常,可调用的序列化相当便宜,这是一般规则的例外情况之一。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-14
  • 2023-02-03
相关资源
最近更新 更多