原作者:Yasoob
原网址:https://pythontips.com/2019/02/26/python-dis-module-and-constant-folding
原日期:February 26,2019
大家好!当我发现:
>>> pow(3,89)
比
>>> 3**89
运行慢时感到十分疑惑
我试图想出一个合适的答案,但时并没有找到。我使用Python3中的timeit模块对这两个语句的执行时间进行计算:
$ python3 -m timeit 'pow(3,89)'
500000 loops, best of 5: 688 nsec per loop
$ python3 -m timeit '3**89'
500000 loops, best of 5: 519 nsec per loop
时间差不大。虽然只有0.1微妙但这还是阻挠了我。如果我不能解释一些编程上的问题,我经常会有不眠之夜?
我在Freenode上的Python IRC频道找到了答案。pow稍微慢一些的原因是在CPython中有一个额外的步骤从命名空间加载pow。然而在3**9中却不需要这样的加载。这也意味着即使输入的数字越来越大,这个差值也会基本保持不变。
假设是成立的:
$ python3 -m timeit 'pow(3,9999)'
5000 loops, best of 5: 58.5 usec per loop
$ python3 -m timeit '3**9999'
5000 loops, best of 5: 57.3 usec per loop
在探索这个问题的解决方案的过程时我也学习了dis模块。它允许您反编译Python字节码并检查它。这是一个令人兴奋的发现,正是因为我现在正在学习关于逆向工程二进制文件的知识,而这个模块正好适合我。
我在Python中这样分解了上述情况的字节码:
>>> import dis
>>> dis.dis('pow(3,89)')
# 1 0 LOAD_NAME 0 (pow)
# 2 LOAD_CONST 0 (3)
# 4 LOAD_CONST 1 (89)
# 6 CALL_FUNCTION 2
# 8 RETURN_VALUE
>>> dis.dis('3**64')
# 1 0 LOAD_CONST 0 (3433683820292512484657849089281)
# 2 RETURN_VALUE
>>> dis.dis('3**65')
# 1 0 LOAD_CONST 0 (3)
# 2 LOAD_CONST 1 (65)
# 4 BINARY_POWER
# 6 RETURN_VALUE
通过阅读Stackoverflow上的这个问题的答案来学习如何理解dis.dis的输出。
好了,现在回到代码。pow的拆解是有意义的。它从命名空间加载pow,然后将3和89加载到register,最后才调用pow函数。但是,为什么后面两个拆解后的输出不同呢?唯一改变的只是指数从64变成了65。
这个问题使我了解了“常数折叠”这个新概念。它意味着,当我们有一个常量表达式时,Python会在编译时计算这个表达式的值,这样,当你真正运行程序时,它不会花费很长时间,因为Python使用了已经计算过的值。你可以这样想:
def one_plue_one():
return 1+1
def one_plue_one():
return 2
Python将第一个函数编译为第二个函数,并在运行代码时使用它,难道不是吗?
那么为什么常数折叠适用于3**64
而不是3**65
呢?好吧,我不知道。这可能与限制系统在内存中预先计算的有一定关联。我可能完全错了。在我看来,下一步是在空闲时间涉猎Python的源代码,并且试图弄清楚发生了什么。我仍然在试图找出这个问题的答案。
我想让你从这篇文章中学到的是,你应该寻求最基本问题的答案。你永远不知道答案会把你带向何方。你也可能会学到一些全新的东西(就像我刚刚做的那样)!我希望你们能永远保持好奇心。下次见!?