Почему math.sqrt намного медленнее, чем возведение в степень? - PullRequest
51 голосов
/ 17 июня 2020

Не могу поверить в то, что я только что измерил:

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

Это противоречит любой интуиции ... должно быть с точностью до наоборот!

Python 3.8.3 на macOS Catalina

Ответы [ 3 ]

78 голосов
/ 17 июня 2020

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)))])'

Обновление : эта конкретная оптимизация теперь применяется непосредственно к абстрактному синтаксическому дереву , поэтому байтовый код создается непосредственно из чего-то вроде

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 оцениваются инструкции (например, нет поиска имени), оптимизатор глазка может взять эту последовательность байтовых кодов, выполнить вычисление самого 2**0.5 и заменить первые три инструкции одной инструкцией LOAD_CONST, которая загружает результат немедленно.

23 голосов
/ 17 июня 2020

Чтобы усилить ответ Чепнера , вот доказательство:

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

vs.

>>> 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 голосов
/ 22 июня 2020
>>> 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=[]))])"
...