Python:为什么*和**比/和sqrt()快?

发布于 2021-01-29 15:09:40

在优化代码时,我意识到了以下几点:

>>> from timeit import Timer as T
>>> T(lambda : 1234567890 / 4.0).repeat()
[0.22256922721862793, 0.20560789108276367, 0.20530295372009277]
>>> from __future__ import division
>>> T(lambda : 1234567890 / 4).repeat()
[0.14969301223754883, 0.14155197143554688, 0.14141488075256348]
>>> T(lambda : 1234567890 * 0.25).repeat()
[0.13619112968444824, 0.1281130313873291, 0.12830305099487305]

并且:

>>> from math import sqrt
>>> T(lambda : sqrt(1234567890)).repeat()
[0.2597470283508301, 0.2498021125793457, 0.24994492530822754]
>>> T(lambda : 1234567890 ** 0.5).repeat()
[0.15409398078918457, 0.14059877395629883, 0.14049601554870605]

我认为它与在C中实现python的方式有关,但我想知道是否有人愿意解释为什么会这样?

关注者
0
被浏览
80
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    结果的(有些出乎意料的原因)是Python似乎折叠了涉及浮点乘法和幂运算而不是除法的常量表达式。math.sqrt()完全是另一种野兽,因为没有字节码,并且涉及函数调用。

    在Python 2.6.5上,以下代码:

    x1 = 1234567890.0 / 4.0
    x2 = 1234567890.0 * 0.25
    x3 = 1234567890.0 ** 0.5
    x4 = math.sqrt(1234567890.0)
    

    编译为以下字节码:

      # x1 = 1234567890.0 / 4.0
      4           0 LOAD_CONST               1 (1234567890.0)
                  3 LOAD_CONST               2 (4.0)
                  6 BINARY_DIVIDE       
                  7 STORE_FAST               0 (x1)
    
      # x2 = 1234567890.0 * 0.25
      5          10 LOAD_CONST               5 (308641972.5)
                 13 STORE_FAST               1 (x2)
    
      # x3 = 1234567890.0 ** 0.5
      6          16 LOAD_CONST               6 (35136.418286444619)
                 19 STORE_FAST               2 (x3)
    
      # x4 = math.sqrt(1234567890.0)
      7          22 LOAD_GLOBAL              0 (math)
                 25 LOAD_ATTR                1 (sqrt)
                 28 LOAD_CONST               1 (1234567890.0)
                 31 CALL_FUNCTION            1
                 34 STORE_FAST               3 (x4)
    

    如您所见,乘法和乘幂根本不需要时间,因为它们是在编译代码时完成的。除法发生在运行时,因此花费的时间更长。平方根不仅是这四个运算中运算量最大的运算,而且还会产生其他运算所没有的各种开销(属性查找,函数调用等)。

    如果消除了恒定折叠的效果,则几乎没有分隔乘法和除法的方法:

    In [16]: x = 1234567890.0
    
    In [17]: %timeit x / 4.0
    10000000 loops, best of 3: 87.8 ns per loop
    
    In [18]: %timeit x * 0.25
    10000000 loops, best of 3: 91.6 ns per loop
    

    math.sqrt(x)实际上比快一点x ** 0.5,大概是因为后者是特例,因此尽管有额外开销也可以更高效地完成:

    In [19]: %timeit x ** 0.5
    1000000 loops, best of 3: 211 ns per loop
    
    In [20]: %timeit math.sqrt(x)
    10000000 loops, best of 3: 181 ns per loop
    

    编辑2011-11-16:
    常量表达式折叠由Python的窥孔优化器完成。源代码(peephole.c)包含以下注释,解释了为什么不折叠常数除法:

        case BINARY_DIVIDE:
            /* Cannot fold this operation statically since
               the result can depend on the run-time presence
               of the -Qnew flag */
            return 0;
    

    -Qnew标志启用PEP 238中定义的“真划分” 。



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看