Адаптивный интерпретатор Python

Смотрим как работает и что дает Specializing Adaptive Interpreter в Python

В версии Python 3.11 среди прочего был представлен механизм специализированного адаптивного интерпретатора. Звучит сложно, но на самом деле все довольно понятно.

Bytecode

В процессе исполнения Python код преобразуется в так называемый байт-код - некоторое промежуточное представление между понятным нам кодом, и непонятным нам машинным кодом.

Python предоставляет удобный инструмент dis, позволяющий посмотреть как исходный Python код будет преобразован в байт-код. Например, возьмем следующий Python код:

def add(x: float, y: float) -> float:
    z = x + y
    return z

Мы можем посмотреть его байт-код представление выполнив следующие команды:

import dis

dis.dis(add, adaptive=True)

Получим следующий вывод:

  4           0 RESUME                   0

  5           2 LOAD_FAST__LOAD_FAST     0 (x)
              4 LOAD_FAST                1 (y)
              6 BINARY_OP                0 (+)
             10 STORE_FAST__LOAD_FAST    2 (z)

  6          12 LOAD_FAST                2 (z)
             14 RETURN_VALUE

Сосредоточимся на одном конкретном блоке этого байт-кода:

              6 BINARY_OP                0 (+)

Объясняя простым языком, BINARY_OP 0 (+) берет два числа и складывает их.

Оптимизация "горячего" кода

Теперь, когда мы немного разобрались с байт-кодом, поговорим наконец об оптимизациях, которые были представлены в Python 3.11 в рамках специализированного адаптивного интерпретатора. Новые оптимизации нацелены на ускорение так называемого "горячего" кода - то есть участков кода, которые выполняются относительно часто.

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

Рассмотрим на примере нашей функции add и в частности оператора сложения, BINARY_OP 0 (+). Если мы N раз вызовем нашу функцию с аргументами типа float и потом посмотрим на вывод байт-кода через dis.dis с флагом adaptive=True, то увидим что инструкция BINARY_OP превратилась в BINARY_OP_ADD_FLOAT - специализированную функцию, которая оптимизирована конкретно для сложения двух чисел типа float:

import dis


def add(x: float, y: float) -> float:
    z = x + y
    return z

for _ in range(7):
    add(10.0, 10.2)

dis.dis(add, adaptive=True)

#   4           0 RESUME                   0

#   5           2 LOAD_FAST__LOAD_FAST     0 (x)
#               4 LOAD_FAST                1 (y)
#               6 BINARY_OP_ADD_FLOAT      0 (+)
#              10 STORE_FAST__LOAD_FAST    2 (z)

#   6          12 LOAD_FAST                2 (z)
#              14 RETURN_VALUE

Почитать подробнее об этих оптимизациях можно в документе PEP 659.

Также, очень рекомендую к просмотру доклад Talks - Brandt Bucher: Inside CPython 3.11's new specializing, adaptive interpreter.