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


80

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

>>> 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的方式有关,但我想知道是否有人愿意解释为什么会这样?


您接受的问题答案(我认为是回答您的真实问题)与您的问题标题无关。您可以将其编辑为与不断折叠有关吗?
Zan Lynx

1
@ZanLynx-嗨 您介意澄清吗?我发现问题标题恰好表示我想知道的内容(为什么X比Y快),而我选择的答案恰恰是……似乎对我来说很完美……但是也许我忽略了某些东西?
mac

8
由于其本质,乘法和幂函数总是比除法和sqrt()函数快。除法和根运算通常必须使用一系列越来越精细的近似,并且不能像乘法可以直接找到正确的答案。
Zan Lynx

我觉得问题标题应该说一个事实,即值都是文字常量,事实证明这是答案的关键。在典型的硬件上,整数和FP乘和加/减比较便宜;integer和FP div以及FP sqrt都很昂贵(可能比FP mul延迟3倍,吞吐量比FP mul糟糕10倍)。(与cube-root或pow()等不同,大多数CPU在硬件中将这些操作作为单个asm指令来实现)。
彼得·科德斯

1
但是,如果Python解释器的开销仍然使mul和div asm指令之间的差异相形见I,我不会感到惊讶。有趣的事实:在x86上,FP除法的性能通常高于整数除法。(agner.org/optimize)。Intel Skylake上的64位整数除法具有42-95个周期的延迟,而32位整数则为26个周期,而双精度FP为14个周期。(64位整数乘以3个周期延迟,FP mul为4)。吞吐量差异甚至更大(int / FP mul和add每个时钟都至少一个,但是除法和sqrt尚未完全流水线化。)
Peter Cordes

Answers:


114

结果的(有些出乎意料的原因)是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中定义的“真划分” 。


2
也许是“保护”以防止被零除?
hugomg 2011年

2
@missingno:我不清楚为什么需要任何这样的“保护”,因为两个参数在编译时都是已知的,所以结果也是(+ inf,-inf,NaN之一)。
NPE

13
常量折叠与工作/在Python 3,并与//在Python 2和3。因此,最有可能这是一个事实的结果/可以在Python 2.不同的含义也许当常量折叠完成,现在还不知道是否from __future__ import division就是有效?
2011年

4
@aix -1./0.在Python 2.7不会导致NaN而是一个ZeroDivisionError
2011年

2
@Caridorc:Python被编译成字节码(.pyc文件),然后由Python运行时解释。字节码与汇编/机器码(例如,C编译器生成的)不同。dis模块可用于检查给定代码片段编译到的字节码。
Tony Suffolk 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.