【问题标题】:Why is math.sqrt massively slower than exponentiation?为什么 math.sqrt 比幂运算慢很多?
【发布时间】:2020-06-17 14:25:24
【问题描述】:

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

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

这违背了任何直觉......它应该完全相反!

macOS Catalina 上的 Python 3.8.3

【问题讨论】:

标签: python performance macos-catalina python-3.8


【解决方案1】:

Python 3 在编译时预先计算 2 ** 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

我不确定是否有办法显示未优化的字节码。 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)))])'

更新:现在应用了这种特殊的优化directly to the abstract syntax tree,因此字节码是直接从类似的东西生成的

Module(body=Num(n= 1.4142135623730951))

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

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

LOAD_CONST       0  (2)
LOAD_CONST       1  (0.5)
BINARY_POWER
RETURN_VALUE

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

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

>>> dis.dis("2**0.5")
  1           0 LOAD_CONST               0 (1.4142135623730951)
              2 RETURN_VALUE

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

由于运行时不会影响两条LOAD_CONST 和后面的BINARY_POWER 指令的评估方式(例如,没有名称查找),因此窥孔优化器可以采用此字节码序列,执行@987654337 的计算@ 本身,并将前三个指令替换为单个 LOAD_CONST 指令,该指令会立即加载结果。

【讨论】:

  • 是的!很有意义!谢谢!
  • 这种优化实际上发生在最近的 Python 版本中的 AST 级别(请参阅Python/ast_opt.c),因此实际上不再生成“未优化”字节码。
  • 啊,好吧,我看到了在 AST 级别进行优化的参考,但认为这是未来的增强,因为 ast 模块(还)没有利用它。
【解决方案2】:

为了增强chepner's answer,这里有一个证明:

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

对比

>>> 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

【讨论】:

    【解决方案3】:
    >>> dis.dis('44442.3123 ** 0.5')
              0 LOAD_CONST               0 (210.81345379268373)
              2 RETURN_VALUE
    

    我不相信44442.3123 ** 0.5 是在编译时预先计算的。我们最好检查一下代码的 AST。

    >>> import ast
    >>> import math
    >>> code = ast.parse("2**2")
    >>> ast.dump(code)
    'Module(body=[Expr(value=BinOp(left=Num(n=2), op=Pow(), right=Num(n=2)))])'
    >>> code = ast.parse("math.sqrt(3)")
    >>> ast.dump(code)
    "Module(body=[Expr(value=Call(func=Attribute(value=Name(id='math', ctx=Load()), attr='sqrt', ctx=Load()), args=[Num(n=3)], keywords=[]))])"
    

    【讨论】:

    • ast.parse 根本不执行窥孔优化。 (我相信优化器使用解析树生成的字节码作为其输入。)
    • @chepner 你是对的。我一直想知道为什么它被编译为加载 const。您更新的答案清楚地解释了这一点。感谢更新。
    猜你喜欢
    • 2011-03-02
    • 2021-01-05
    • 2015-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多