映射单个函数比两次映射两个单独的函数慢?

Mapping a single function is slower than twice mapping two separate functions?

下面的例子好像在暗示运行我不明白的时间优化

谁能解释这种行为以及它如何适用于更一般的情况?

例子

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

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 代码,因此在必须解释嵌套函数时受到限制?

区别在于map()是用C代码实现的,调用其他C实现的函数便宜,而调用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) 版本,尽管它也必须使用 global;如果您也将时间测试设为本地,它仍然可以轻而易举地击败函数调用:

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

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

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

如图所示,map(y, l) 执行,每个输入值:

  • y创建栈帧,执行正文
    • 查找 str 作为全局
      • y堆栈帧压入堆栈
      • 执行str(...)
      • 从堆栈弹出堆栈帧
    • return 结果

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

  • _y 创建堆栈框架
    • return 结果
  • 执行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

_y 上调用 map() 两次比在另一个函数中嵌套 _y(_y(x)) 调用更快,后者必须进行全局名称查找并强调 Python 再叠一些;在您的 f() 示例中,每个 map() 迭代必须创建 3 个堆栈帧并将它们推入和弹出堆栈,而在您的 map(_y, map(_y, ...)) 设置中,每个迭代项仅创建 2 个帧:

  • f创建栈帧,执行正文
    • 查找 _y 作为全局
      • f堆栈帧压入堆栈
      • _y创建栈帧,执行正文
      • 从堆栈弹出堆栈帧
    • 查找 _y 作为全局(是的,再次)
      • f堆栈帧压入堆栈
      • _y创建栈帧,执行正文
      • 从堆栈弹出堆栈帧
    • return 结果

对比:

  • _y创建栈帧,执行正文
    • return 结果
  • _y创建栈帧,执行正文
    • return 结果

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

>>> 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 堆栈帧和全局名称查找。