为什么 math.sqrt 比幂运算慢得多?

Pet*_*auf 52 python performance python-3.8 macos-catalina

我简直不敢相信我刚刚测量的结果:

python3 -m timeit -s "from math import sqrt" "sqrt(2)"
5000000 loops, best of 5: 42.8 nsec per loop

python3 -m timeit "2 ** 0.5"
50000000 loops, best of 5: 4.93 nsec per loop
Run Code Online (Sandbox Code Playgroud)

这违背了任何直觉……它应该正好相反!

macOS Catalina 上的 Python 3.8.3

che*_*ner 80

Python 32 ** 0.5在编译时预先计算 的值,因为当时两个操作数都是已知的。的价值sqrt,但是,没有在编译时已知,所以计算必然在运行时发生。

您不是在计算计算所需2 ** 0.5的时间,而只是计算加载常量所需的时间。

更公平的比较是

$ python3 -m timeit -s "from math import sqrt" "sqrt(2)"
5000000 loops, best of 5: 50.7 nsec per loop
$ python3 -m timeit -s "x = 2" "x**0.5"
5000000 loops, best of 5: 56.7 nsec per loop
Run Code Online (Sandbox Code Playgroud)

我不确定是否有办法显示未优化的字节码。Python 首先将源代码解析为抽象语法树 (AST):

>>> ast.dump(ast.parse("2**0.5"))
'Module(body=[Expr(value=BinOp(left=Num(n=2), op=Pow(), right=Num(n=0.5)))])'
Run Code Online (Sandbox Code Playgroud)

更新:这个特殊的优化现在直接应用于抽象语法树,所以字节码是直接从类似的东西中生成的

Module(body=Num(n= 1.4142135623730951))
Run Code Online (Sandbox Code Playgroud)

ast模块似乎没有应用优化。

编译器采用 AST 并生成未优化的字节码;在这种情况下,我认为它看起来(基于输出dis.dis("2**x")dis.dis("x**0.5"))像

LOAD_CONST       0  (2)
LOAD_CONST       1  (0.5)
BINARY_POWER
RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

然后原始字节码会被窥孔优化器修改,这可以将这 4 条指令减少到 2 条,如dis模块所示。

然后编译器从 AST 生成字节码。

>>> dis.dis("2**0.5")
  1           0 LOAD_CONST               0 (1.4142135623730951)
              2 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

[虽然以下段落最初是根据优化字节码的想法编写的,但推理也适用于优化 AST。]

因为在运行时没有任何东西影响如何评估两条LOAD_CONST和后面的BINARY_POWER指令(例如,没有名称查找),窥孔优化器可以采用这个字节码序列,执行2**0.5自己的计算,并用单个指令替换前三个指令LOAD_CONST立即加载结果的指令。


mkr*_*er1 25

为了加强chepner 的回答,这里有一个证明:

Python 3.5.3 (default, Sep 27 2018, 17:25:39) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> dis.dis('2 ** 0.5')
  1           0 LOAD_CONST               2 (1.4142135623730951)
              3 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

对比

>>> dis.dis('sqrt(2)')
  1           0 LOAD_NAME                0 (sqrt)
              3 LOAD_CONST               0 (2)
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)