以下示例似乎意味着我不理解的运行时优化
任何人都可以解释这种行为以及它如何适用于更一般的案例吗?
考虑以下简单(示例)函数
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代码,因此在必须解释嵌套函数时会受到限制吗?
答案 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堆栈帧和全局名称查找。