其他分享
首页 > 其他分享> > 映射单个函数要慢于两次映射两个单独的函数?

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

作者:互联网

以下示例似乎暗示了我不理解的运行时优化

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

考虑以下简单(示例)功能

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

结果

映射操作将必须遍历列表中的所有元素.似乎反直观,将函数拆分为双图会明显优于单图

%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的开销问题吗,在这种情况下,地图会尽可能地编译低级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)版本,即使它也必须使用全局变量.如果您也给时间测试一个本地变量,它仍然可以胜过函数调用:

>>> 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

在_y上调用map()两次比在另一个函数中嵌套_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:与double map()版本相比,您的y()函数会遭受O(N)个额外的全局名称查找和O(N)个额外的堆栈框架对象被推入和退出Python堆栈的痛苦.

如果速度对这个匹配很重要,请尝试避免在紧密循环中创建Python堆栈框架和全局名称查找.

标签:nested-function,performance,python-2-7,iteration,python
来源: https://codeday.me/bug/20191111/2019288.html