映射单个函数比两次映射两个单独的函数慢?
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 堆栈帧和全局名称查找。
下面的例子好像在暗示运行我不明白的时间优化
谁能解释这种行为以及它如何适用于更一般的情况?
例子
考虑以下简单(示例)函数
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 堆栈帧和全局名称查找。