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]

我认为这与Python在C中的实现方式有关,但我想知道是否有人能解释一下为什么会这样?


您接受的回答(我假设它回答了您真正的问题)与您的问题标题没有太大关系。您能否修改它,使其与常量折叠有关? - Zan Lynx
1
@ZanLynx - 你好。您能否澄清一下吗?我发现问题标题恰好表达了我想知道的内容(为什么X比Y更快),而我选择的答案也恰好回答了这个问题... 在我看来是完美的匹配... 但也许我忽略了什么? - mac
8
由于它们的本质不同,乘方和乘法函数总是比除法和sqrt()函数更快。除法和根运算通常必须使用一系列越来越精细的近似值,并且不能像乘法那样直接得到正确答案。 - Zan Lynx
我觉得问题标题应该提到所有值都是字面常量这一事实,这对答案非常关键。在典型的硬件上,整数和浮点数的乘法、加/减法很便宜;整数和浮点数的除法以及浮点数的平方根则非常昂贵(可能比浮点数乘法的延迟差3倍,吞吐量差10倍)。 (大多数CPU将这些操作作为单个汇编指令在硬件中实现,而不像立方根或pow()等函数)。 - Peter Cordes
1
但是如果Python解释器的开销仍然超过了mul和div汇编指令之间的差异,我也不会感到惊讶。有趣的事实是:在x86上,FP除法通常比整数除法性能更高。(http://agner.org/optimize/)。在Intel Skylake上进行64位整数除法的延迟为42-95个时钟周期,而32位整数为26个周期,双精度FP为14个周期。 (64位整数乘法为3个周期延迟,FP乘法为4个周期)。吞吐量差异甚至更大(int / FP乘法和加法至少每个时钟周期一次,但除法和sqrt不完全流水线化)。 - Peter Cordes
1个回答

115

你得到结果的原因(可能出乎意料)是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优化器执行常量表达式折叠。源代码(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
2
@missingno:对我来说不清楚为什么需要任何这样的“保护”,因为两个参数在编译时就已知,结果也是如此(它是以下之一:+inf、-inf、NaN)。 - NPE
14
在Python 3中,常量折叠(constant folding)可以与“/”一起使用,在Python 2和3中则可以与“//”一起使用。因此,这很可能是由于“/”在Python 2中具有不同的含义所致。也许在进行常量折叠时,还不知道是否已经启用了“from future import division”? - interjay
4
在Python 2.7中,1./0.不会得到NaN,而是会报出ZeroDivisionError - detly
2
@Caridorc:Python会被编译成字节码(.pyc文件),随后由Python运行时解释执行。字节码与汇编/机器码不同(例如C编译器所生成的内容)。可以使用dis模块来检查给定代码片段编译成的字节码。 - Tony Suffolk 66
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接