company_banner

Модуль dis в Python и свертка констант

Автор оригинала: pythontips
  • Перевод

Всем привет. Сегодня хотим поделиться еще одним переводом подготовленным в преддверии запуска курса «Web-разработчик на Python». Поехали!



Недавно я очень удивился, когда обнаружил, что


>>> pow(3,89)

работает медленнее, чем


>>> 3**89

Я пытался придумать какое-либо приемлемое объяснение, но не смог. Я засек время выполнения этих двух выражений, используя модуль timeit из Python 3:


$ python3 -m timeit 'pow(3,89)'
500000 loops, best of 5: 688 nsec per loop

$ python3 -m timeit '3**89'
500000 loops, best of 5: 519 nsec per loop

Различие небольшое. Всего 0,1 мкс, но это не давало мне покоя. Если я не могу объяснить что-нибудь в программировании, я начинаю страдать бессонницей


Я нашел ответ с помощью канала Python IRC на Freenode. Причина, по которой pow работает чуть-чуть медленнее заключается в том, что уже в CPython появляется дополнительный шаг загрузки pow из пространства имен. Тогда как при вызове 3**9 такая загрузка не нужна в принципе. Также это значит, что эта разница во времени останется более-менее постоянной, если входные значения будут расти.


Гипотеза подтвердилась:


$ python3 -m timeit 'pow(3,9999)'
5000 loops, best of 5: 58.5 usec per loop

$ python3 -m timeit '3**9999'
5000 loops, best of 5: 57.3 usec per loop

В процессе поиска решения этого вопроса я также узнал о модуле dis. Он позволяет декомпилировать байт-код Python и изучить его. Это было крайне захватывающее открытие, поскольку в последнее время я изучаю реверс-инжиниринг двоичных фалов, а обнаруженный модуль пришелся как раз кстати в этом вопросе.


Я декомпилировал байт-код выражений, приведенных выше, и получил следующее:


>>> import dis
>>> dis.dis('pow(3,89)')
#  1           0 LOAD_NAME                0 (pow)
#              2 LOAD_CONST               0 (3)
#              4 LOAD_CONST               1 (89)
#              6 CALL_FUNCTION            2
#              8 RETURN_VALUE

>>> dis.dis('3**64')
#  1           0 LOAD_CONST               0 (3433683820292512484657849089281)
#              2 RETURN_VALUE

>>> dis.dis('3**65')
#  1           0 LOAD_CONST               0 (3)
#              2 LOAD_CONST               1 (65)
#              4 BINARY_POWER
#              6 RETURN_VALUE

Вы можете почитать о том, как правильно понимать выходные данные dis.dis, обратившись к этому ответу на Stackoverflow.


Хорошо, вернемся к коду. В декомпиляции pow есть смысл. Байт-код загружает pow из пространства имен, загружает в регистры 3 и 89, и, наконец, вызывает функцию pow. Но почему выходные данные следующих двух декомпиляций отличаются друг от друга? Ведь все, что мы изменили, это значение показателя степени с 64 на 65!


Этот вопрос познакомил меня еще с одним новым понятием, которое называется «свертка констант». Оно означает, что когда у нас есть константное выражение, Python вычисляет его значение на этапе компиляции, так что когда вы запустите программу, ее работа не займет много времени, поскольку Python использует уже вычисленное значение. Взгляните на это:


def one_plue_one():
     return 1+1

# --vs--

def one_plue_one():
     return 2

Python компилирует первую функцию во вторую и использует ее при запуске кода. Неплохо, да?


Так почему свертка констант работает для 3**64, но не для 3**65? Что ж, я не знаю. Вероятно, это как-то связано с ограничением количества степеней, предварительно вычисленных системой в памяти. Я могу ошибаться. Следующий шаг, который я планирую предпринять, это порыться в исходном коде Python в свободное время и попытаться понять, что происходит. Я все еще ищу ответ на свой вопрос, поэтому, если у вас есть какие-то идеи, делитесь ими в комментариях.


Мне хочется, чтобы из этого поста вы почерпнули вдохновение на поиск решения своих вопросов самостоятельно. Вы никогда не знаете, куда вас приведут ответы. В конечном итоге вы можете узнать что-то совершенно новое, как случилось со мной. Надеюсь, в вас еще горит пламя любопытства!


Замечали похожие штуки? Ждём ваших комментариев!

OTUS. Онлайн-образование
556,57
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Похожие публикации

Комментарии 4

    0
    Кстати в моем случае одинаковы измерения:
    python -m timeit 'pow(3,89)'
    100000000 loops, best of 3: 0.0164 usec per loop

    python -m timeit '3**89'
    100000000 loops, best of 3: 0.0164 usec per loop
      0
      Раз это свёртка, значит уже ранее выполнялось. Ан нет, действительно все до 64 как будто уже предвычислены. До dis.dis('4**42') свертка, а потом уже нет. 5**, 6**,7** как и 4, до dis.dis('8**32').
      P.S. Если я правильно понял, тут отвечают в чем разница
      stackoverflow.com/questions/55944926/what-are-the-specific-rules-for-constant-folding
      +1

      Кстати, вот буквально пару дней назад написал небольшую статейку для начинающих об анализе и преобразовании абстрактных синтаксических деревьев в Python. Думал, какой бы пример реализовать, написал в итоге генерацию LaTeX, но на самом деле достаточно несложно сделать декоратор, который бы находил в функции выражения и предвычислял их.

        0
        Прикольная тема, так можно встраивать исполняемые формулы в латех.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое