Python的dis模块和常数的卷积

大家好 今天,我们希望分享另一种翻译,该翻译是为课程“ Python中的Web开发人员”的推出而准备的。 走吧



我最近发现我感到非常惊讶


>>> pow(3,89) 

工作慢于


 >>> 3**89 

我试图提出一些可以接受的解释,但是没有。 我使用Python 3中的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μs,但它困扰了我。 如果我什么都不能解释,那我就开始失眠了


我在Freenode上使用Python IRC feed找到了答案。 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字节码并学习它。 这是一个非常令人兴奋的发现,因为最近我一直在研究二进制文件的逆向工程,并且发现的模块在此方面非常有用。


我反编译了上面表达式的字节码,并得到了以下内容:


 >>> 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,加载到寄存器3和89,最后调用pow函数。 但是,接下来的两个反编译的输出为何不同? 毕竟,我们所做的只是将指数的值从64更改为65!


这个问题向我介绍了另一个新概念,称为“常数卷积”。 这意味着当我们有一个常量表达式时,Python会在编译阶段计算其值,因此在您运行程序时,它不会花费很多时间,因为Python使用了已经计算出的值。 看一下这个:


 def one_plue_one(): return 1+1 # --vs-- def one_plue_one(): return 2 

Python将第一个函数编译为第二个函数,并在运行代码时使用它。 还不错吧?


那么为什么常数的卷积对3 ** 64有效,而对3 ** 65无效呢? 好吧,我不知道。 这可能与某种程度上与系统先前在内存中计算的度数限制有关。 我可能是错的。 我计划采取的下一步是在业余时间深入研究Python源代码,并尝试了解发生了什么。 我仍在寻找问题的答案,因此,如果您有任何想法,请在评论中分享。


我希望您从这篇文章中汲取灵感,自己找到解决问题的方法。 您永远不知道答案将引导您到哪里。 最终,您可以学到全新的知识,就像我发生的那样。 我希望好奇心仍在燃烧!


您是否注意到类似的事情? 等待您的评论!

Source: https://habr.com/ru/post/zh-CN460143/


All Articles