映射单个函数比映射两个单独函数的速度慢两倍?

时间:2017-05-19 13:59:35

标签: python performance python-2.7 iteration nested-function

以下示例似乎意味着我不理解的运行时优化

任何人都可以解释这种行为以及它如何适用于更一般的案例吗?

实施例

考虑以下简单(示例)函数

def y(x): # str output
    y = 1 if x else 0
    return str(y)

def _y(x): # no str
    y = 1 if x else 0
    return y

假设我想对列表中的所有元素应用函数y

l = range(1000) # test input data

结果

map操作必须遍历列表中的所有元素。将函数分解为双map明显优于单map函数

似乎是违反直觉的。
%timeit map(str, map(_y, l))
1000 loops, best of 3: 206 µs per loop

%timeit map(y, l)
1000 loops, best of 3: 241 µs per loop

更一般地说,这也适用于非标准库嵌套函数,例如

def f(x):
    return _y(_y(x))

%timeit map(_y, map(_y, l))
1000 loops, best of 3: 235 µs per loop
%timeit map(f, l)
1000 loops, best of 3: 294 µs per loop

这是一个python开销问题,其中map在可能的情况下编译低级python代码,因此在必须解释嵌套函数时会受到限制吗?

1 个答案:

答案 0 :(得分:3)

不同之处在于map()在C代码中实现,并且调用其他C实现的函数是 cheap ,而调用Python代码则很昂贵。最重要的是,从Python代码中调用其他可调用对象也很昂贵:

>>> timeit.timeit('f(1)', 'def f(x): return str(x)')
0.21682000160217285
>>> timeit.timeit('str(1)')
0.140916109085083

,第三,您将函数对象传递给map()(因此不再进行进一步查找),但y()必须每次都查找str名称。与本地查找相比,全局查找相对昂贵;将全局绑定到函数参数以使其成为本地可以帮助抵消这一点:

>>> timeit.timeit('f(1)', 'def f(x, _str=str): return _str(x)')
0.19425392150878906

更接近str(1)版本,即使必须使用全局;如果你把时间测试给了当地人,它仍然可以击败函数调用:

>>> timeit.timeit('_str(1)', '_str = str')
0.10266494750976562

因此,Python字节码执行需要为每个调用创建一个额外的对象,即堆栈帧。在调用其他代码时,必须在专用的Python调用堆栈上管理该堆栈帧对象。此外,您的y函数每次都会将str名称查找为全局,而map(str, ...)调用会保留对该对象的单个引用并反复使用它。

通过将str()调用移出y函数并让map()直接调用str()代替单引用,您删除了堆栈处理和全局名称查找并略微加快了速度。

作为图表,map(y, l)按输入值执行:

  • y创建堆栈框架,执行正文
    • str视为全局
      • y stackframe推入堆栈
      • 执行str(...)
      • 从堆栈中弹出堆栈框架
    • 返回结果

map(str, map(_y, l))执行

  • _y创建堆栈框架
    • 返回结果
  • 执行str(...)

这同样适用于您的f()功能设置:

>>> def f(x):
...     return _y(_y(x))
...
>>> timeit.timeit("map(_y, map(_y, l))", 'from __main__ import _y, testdata as l', number=10000)
2.691640853881836
>>> timeit.timeit("map(f, l)", 'from __main__ import f, testdata as l', number=10000)
3.104063034057617

map()上调用_y两次比在另一个函数中嵌套_y(_y(x))调用更快,然后必须执行全局名称查找并更多地强调Python堆栈;在f()示例中,每个map()迭代必须创建3个堆栈帧并在堆栈中推送和弹出这些帧,而在map(_y, map(_y, ...))设置中,每个迭代项只创建2个帧:

  • f创建堆栈框架,执行正文
    • _y视为全局
      • f stackframe推入堆栈
      • _y创建堆栈框架,执行正文
      • 从堆栈中弹出堆栈框架
    • _y视为全局(是的,再次)
      • f stackframe推入堆栈
      • _y创建堆栈框架,执行正文
      • 从堆栈中弹出堆栈框架
    • 返回结果

  • _y创建堆栈框架,执行正文
    • 返回结果
  • _y创建堆栈框架,执行正文
    • 返回结果

同样,使用本地人可以稍微抵消差异:

>>> def f(x, _y=_y):
...     return _y(_y(x))
...
>>> timeit.timeit("map(f, l)", 'from __main__ import f, testdata as l', number=10000)
2.981696128845215

但是额外的Python框架对象仍在徘徊单map(f, ...)次调用。

TLDR :您的y()函数遭受O(N)额外的全局名称查找和O(N)额外的堆栈框架对象被推入和移出Python堆栈,与之相比双map()版。

如果速度与此匹配相关,请尽量避免在紧密循环中创建Python堆栈帧和全局名称查找。