【问题标题】:Performance overhead of nested functions in PythonPython中嵌套函数的性能开销
【发布时间】:2021-09-09 05:30:33
【问题描述】:

在 Python 3.9 中,嵌套函数比普通函数慢得惊人,在我的示例中约为 10%。

from timeit import timeit

def f():
    return 0

def factory():
    def g():
        return 0

    return g

g = factory()

print(timeit("f()", globals=globals()))
#> 0.074835498
print(timeit("g()", globals=globals()))
#> 0.08470309999999998

dis.dis 显示相同的字节码,我发现的唯一区别在于函数内部标志。事实上,dis.show_code 揭示了 g 有一个标志 NESTEDf 没有。

但是,可以删除标志,它使gf 一样快。

import inspect
g.__code__ = g.__code__.replace(co_flags=g.__code__.co_flags ^ inspect.CO_NESTED)
print(timeit("f()", globals=globals()))
#> 0.07321161100000001
print(timeit("g()", globals=globals()))
#> 0.07439838800000001

我试图查看 CPython 代码以了解 CO_NESTED 标志如何影响函数执行,但我什么也没找到。与CO_NESTED 标志相比,这种性能差异有什么解释吗?

编辑:删除CO_NESTED 标志似乎也对函数执行没有影响,除了开销,即使它已捕获变量。

import inspect
global_var = 40
def factory():
    captured_var = 2
    def g():
        return global_var + captured_var
    return g
g = factory()
assert g() == 42

g.__code__ = g.__code__.replace(co_flags=g.__code__.co_flags ^ inspect.CO_NESTED)
assert g() == 42  # function still works as expected

【问题讨论】:

    标签: python python-3.x performance optimization


    【解决方案1】:

    我可能错了,但我认为不同之处在于g 可以潜在地引用factory 的本地变量,因此需要访问两个范围以进行任何变量查找:globals as以及factory。很可能是保护这个额外的范围(或合并来自factoryglobals 的范围)是您观察到的开销的原因。一个很好的提示是,如果你嵌套了另一层函数:

    def factory():
        def ff():
            def g():
                return 0
    
            return g
        return ff()
    
    g = factory()  # please note that it is equivalent from the perspective of time measurement
    

    时间安排:

    print(timeit("f()", globals=globals(), number=100000000))
    # > 6.792911
    print(timeit("g()", globals=globals(), number=100000000))
    # > 7.8184555
    

    在你的第一个计时案例中,我得到 +5.7%(你的数字是 +13.5%),在我的第二个例子中:+15.1%。

    【讨论】:

    • 感谢您的回答。但是,我已经对其进行了测试,与一层工厂相比,添加第二层工厂并不会增加开销。我知道关于timeit 的结果很难讨论,因为测试太快了,所以受外部进程干扰的影响太大。
    • 关于范围,我认为不会有合并,因为全局变量和嵌套变量的处理方式不同。全局范围存储在__globals__ 属性中,并通过LOAD_GLOBAL 字节码访问,而嵌套范围被“捕获”在内部结构中,名称在__code__.co_freevars 中,值在__closure__ 中,并通过LOAD_DEREF 字节码访问。
    • 我已经编辑了我的问题,以明确在捕获变量时删除标志不会影响执行,因此取决于CO_NESTED 的可能额外处理似乎与范围分辨率无关.但我也可能错了。
    • 关于检查操作码等的好点。请注意,机器之间的确切实现可能不同(我在wintel上运行实验),也可能有缓存对齐等,取决于数量整个脚本的代码。这里有很多变量会影响执行......
    猜你喜欢
    • 2012-12-12
    • 2011-12-11
    • 2013-11-15
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-24
    相关资源
    最近更新 更多