Python 3.15's JIT is now back on track

18.03.2026 · 5 мин

Знаете, что меня всегда удивляло? CPython — самый популярный Python в мире — десятилетиями работал без JIT-компилятора (Just-In-Time — компиляция на лету прямо во время выполнения программы). Все вокруг говорили о скорости, а CPython честно интерпретировал байт-код строчка за строчкой.

И вот в 3.13 появился долгожданный JIT. Но радость была недолгой: он часто работал медленнее обычного интерпретатора! Можно представить разочарование разработчиков: годы работы — и такой результат.

А теперь новости говорят, что Python 3.15 уже обгоняет интерпретатор на 5-12% в зависимости от платформы. Как так вышло? Давайте разберёмся.

От триумфа к провалу

CPython 3.13 вышел с первым экспериментальным JIT. Команда Faster CPython во главе с Марком Шенноном работала над ним несколько лет. Но результаты оказались удручающими:

ПРОИЗВОДИТЕЛЬНОСТЬ CPYTHON JIT 3.xx
─────────────────────────────────────
3.13    ████░░░░░░░░░░░░░░ - Часто медленнее интерпретатора!
3.14    ██████░░░░░░░░░░ - Небольшие улучшения  
3.15    ████████████ - Цель достигнута досрочно!

        └───────▶ Улучшение скорости ───▶ 
Динамика производительности JIT по версиям

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

Автор статьи Кен Джин пишет честно: он всерьёз сомневался, стоит ли продолжать.

Переосмысление подхода

Вместо того чтобы героически «спасать проект», команда поступила умнее — они пересмотрели саму архитектуру.

Trace recording (запись трассы выполнения) — это один из ключевых компонентов любого JIT-компилятора. Идея простая: вместо компиляции всего подряд отслеживай «горячие» участки кода (те, что выполняются много раз) и оптимизируй только их.

Кен описывает забавную историю: коллега Брандт Бучер буквально «подстрекнул» его переписать переднюю часть JIT (frontend — та часть, которая анализирует код). Кен хотел доказать, что идея не работает… и случайно создал решение лучше оригинального!

Случайность привела к механизму под названием dual dispatch (двойная диспетчеризация). Суть в том, чтобы минимально увеличивать размер интерпретатора при добавлении возможностей трассировки:

ДВОЙНАЯ ДИСПЕТЧЕРИЗАЦИЯ (Dual Dispatch)
─────────────────────────────────────────

Оригинальный подход:
┌─────────────────┐     ┌─────────────────┐
│ Интерпретатор   │ +   │ Трассирующий    │
│ (1000 инстр.)   │     │ интерпретатор   │
└─────────────────┘     └─────────────────┘
= ~2000 инструкций (2x раздувание!)

Подход dual dispatch:
┌─────────────────────────────┐     
│ Интерпретатор               │     
│ + 1 специальная инструкция  │     
└─────────────────────────────┘     
= Минимальные накладные расходы

Ключевая идея: одна инструкция управляет всей логикой трассировки!
Почему dual dispatch эффективнее классического подхода

Это дало +50% к покрытию кода JIT! То есть все последующие оптимизации стали работать примерно вдвое эффективнее.

Reference count elimination

Вторая ключевая оптимизация связана с подсчётом ссылок (reference counting) — механизмом автоматической памяти в Python.

Каждый раз когда Python создаёт или уничтожает объект, ему нужно обновить счётчик ссылок:

// Примерно так выглядит каждая операция в байт-коде:
// Py_DECREF(obj); // уменьшить счётчик ссылок → проверить if (--refcount == 0) → освободить память

// Для каждой операции генерируется ветвление:
// if (refcount != 1) { ... } // проверка перед освобождением

Проблема: эти проверки генерировались для КАЖДОЙ операции! Даже одна лишняя проверка (branch) может стоить нескольких тактов процессора, умноженных на миллионы выполнений…

Команда смогла устранить большинство таких проверок через анализ жизненного цикла объектов на этапе компиляции JIT.

Сила сообщества

Вот что меня действительно зацепило в статье: команда сознательно работала над bus factor (количество людей, знающих проект достаточно хорошо).

Bus factor показывает, сколько ключевых людей держат проект в голове. Если они одновременно «попадут под автобус» и исчезнут из проекта, работа может встать:

BUS FACTOR В ПРОЕКТЕ CPYTHON JIT
    
До начала работ:
    Middle-end: ██ = 2 человека
    
После кампании по привлечению:
    Frontend + Middle-end + Backend = ~10 активных контрибьюторов
    
Ключ к успеху:
✓ Разбиение задач на маленькие куски ("оптимизируй одну инструкцию")
✓ Подробные инструкции для новичков  
✓ Публичное празднование достижений любого размера
    
Философия: "JIT можно превратить из непрозрачного blob'а 
            во что-то понятное C-программисту без опыта в компиляторах"
Рост bus factor снизил зависимость JIT-команды от нескольких ключевых людей

Это сработало! Одиннадцать человек приняли участие только в конвертации инструкций интерпретатора в более дружественный для JIT формат.

Результаты и перспективы

Цифры говорят сами за себя:

Платформа Ускорение vs Интерпретатор
macOS AArch64 11-12%
x86_64 Linux 5-6%

Но это средние значения. Диапазон реальных результатов — от -20% до +100% в зависимости от конкретного бенчмарка.

Чего ещё не хватает? Поддержки free-threading — режима работы без глобальной блокировки интерпретатора (GIL), который позволит использовать несколько ядер процессора одновременно для разных потоков Python-кода.

Команда планирует добавить это либо в 3.15, либо в 3.16.

Выводы

Из этой истории я вынес три вещи.

Первая: JIT не становится быстрым только потому, что его наконец включили в релиз. Команде CPython пришлось заново собирать фронтенд трассировки и убирать лишние расходы на каждом горячем участке.

Вторая: организационные решения сработали не хуже технических. Рост bus factor и разбиение задач на маленькие куски сделали проект устойчивее именно в тот момент, когда он потерял финансирование.

Третья: Python пока не превратился в чемпиона по скорости, но траектория уже изменилась. Если free-threading доедет в 3.15 или 3.16, это будет не косметическое ускорение, а заметный сдвиг для всего CPython.

Ссылки