Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 17 из 20 Средний 15 мин 100 XP

PyPy: JIT-компилятор Python и когда он выигрывает

Тот же язык, другой движок: PyPy компилирует горячие участки прямо во время работы. Разбираем разогрев, ускорение в разы, совместимость с C-расширениями и случаи, когда CPython остаётся правильным выбором.

Редакция Питоники

Урок 16 поставил PyPy в таблицу реализаций со сноской про JIT. Теперь разворачим строку целиком: как JIT-компиляция устроена изнутри, почему она даёт ускорение в разы именно на чистом Python, чего стоит разогрев и где границы совместимости, из-за которых PyPy до сих пор не стал интерпретатором по умолчанию.

Два движка: пошаговый интерпретатор и JIT

CPython исполняет байткод по одной инструкции: прочитал, выполнил, пошёл дальше. У строки кода, которая выполняется миллион раз, каждая итерация проходит этот цикл заново. JIT — just-in-time компиляция — работает иначе: движок замечает горячие участки, которые выполняются снова и снова, компилирует их в машинный код процессора — и дальнейшие итерации идут мимо интерпретатора напрямую. Проверим сценарий двух прогонов одной и той же функции:

три прогона одной функции
def total_price(prices):
    total = 0
    for p in prices:
        total = total + p
    return total

prices = [120, 90, 240, 60, 310, 45, 500]

print('первый прогон:', total_price(prices))
print('второй прогон:', total_price(prices))
print('третий прогон:', total_price(prices))
Вывод
первый прогон: 1365
второй прогон: 1365
третий прогон: 1365

Результаты одинаковы — язык гарантирует поведение, а различие прячется во времени. На CPython все три прогона заняли бы почти одно и то же время. На PyPy первый прогон был бы заметно медленнее остальных: он тратит время не только на работу, но и на наблюдение и компиляцию, зато разогретые прогоны идут в машинном коде в разы быстрее. В песочнице разницы во времени почти нет — и это честно: интерпретатор здесь CPython, а не PyPy. Конкретные времена мы в печать не выносим сознательно: они зависят от машины и версии, а вот результаты — переносимая величина.

СредаКак исполняется код
CPythonБайткод по одной инструкции на виртуальной машине
PyPyИнтерпретация + компиляция горячих участков в машинный код прямо во время работы
Java, C#Тот же JIT-принцип: разогрев, горячие методы, машинный код
Nuitka из урока 12Заранее в C и машинный код, без разогрева, но и без наблюдения за реальным профилем

Сравнение с Nuitka из урока 12 полезное: оба ускоряют Python, но в противоположных направлениях. Nuitka компилирует всё заранее, до запуска; PyPy наблюдает за настоящей работой и вкладывается только в то, что реально горячее. Урок 15 показывал, что байткод CPython — текст для его виртуальной машины; PyPy читает тот же язык, но строит собственные внутренние представления.

Типичная задача PyPy: долгий чистый цикл

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

сумма кратных 3 или 5 — тяжёлый чистый цикл
total = 0
for i in range(1, 200001):
    if i % 3 == 0 or i % 5 == 0:
        total += i
print('сумма кратных 3 или 5 от 1 до 200000:', total)
Вывод
сумма кратных 3 или 5 от 1 до 200000: 9333366668

Двести тысяч итераций, и ни одна строка не выходит за пределы языка. На CPython цикл отрабатывает за секунды — интерпретатор честно проживает каждую инструкцию. На PyPy после разогрева тот же цикл ускоряется в разы: компилятор переводит условие и сложение в машинный код и убирает всю обвязку интерпретации. Насколько именно в разы — зависит от машины и версии, поэтому в уроке печатается только результат: он одинаковый на обоих движках, и в этом суть — PyPy ускоряет исполнение, не меняя семантику.

Код узнаёт PyPy по sys.implementation

Ветвление из урока 16 работает и для PyPy: имя реализации попадает в сравнение, и код может выбирать путь под движок:

проверка: мы на PyPy?
import sys

print('мы на PyPy?', sys.implementation.name == 'pypy')
print('реализация:', sys.implementation.name)
Вывод
мы на PyPy? False
реализация: cpython

В песочнице и локально ответ False — среда CPython, и это не ошибка примера, а его содержание: у вас нет причин притворяться, что движок другой. На настоящем PyPy первая строка напечатала бы True. Нужны такие проверки редко: обычному коду всё равно, кто его исполняет, — ветвления по реализации живут на границах, например, чтобы включить особый путь оптимизации или обойти несовместимость.

Совместимость: что переезжает, а что нет

Главное правило: чистый Python переносится без правок. PyPy исполняет тот же язык, что и CPython: код, написанный на чистом Python, переезжает без единой правки. Стандартная библиотека тоже на месте. Трудности начинаются на границе с машинным кодом — там, где пакеты используют C API CPython напрямую:

ЧтоНа PyPy
Чистый PythonРаботает как есть, ускоряется JIT-ом
Стандартная библиотекаЕсть: часть на чистом Python, часть с совместимыми заменами
C-расширения через C APIПоддерживаются ограниченно и работают медленно: режим совместимости
cffiРекомендованный путь к библиотекам на C: быстрый и родной для PyPy
pip-пакетыСвоя копия пакетов, отдельная от CPython: бинарники не переезжают

Именно C-расширения — причина, по которой PyPy остаётся нишевым. Большие пакеты анализа данных держат вычисления в машинном коде и так, и на них ускорение PyPy отступает на второй план, а режим совместимости с C API даже замедляет работу. Экосистема PyPy отвечает на это cffi — способом вызывать библиотеки на C без C API, — но переучивать весь мир пакетов никто не стал. Команды у PyPy свои, со своим набором пакетов:

команды на машине с PyPy
pypy -m pip install cffi
pypy app.py
Команды для терминала машины, где установлен PyPy: у него своё имя исполняемого файла и своя среда пакетов, отдельная от CPython. В песочнице этих команд нет — там всегда работает CPython.

Алгоритм важнее движка

JIT ускоряет исполнение, но не исправляет алгоритм: сумма квадратов по списку в миллион элементов умножением быстрее, чем возведение в степень, на любом движке. Проверим, что оба варианта дают одинаковый результат — скорость различается, семантика нет:

два алгоритма, один результат
def slow_sum(points):
    s = 0
    for x, y in points:
        s = s + x * x + y * y
    return s

def fast_sum(points):
    return sum(x * x + y * y for x, y in points)

points = [(i, i + 1) for i in range(1000)]
print('результаты совпадают?', slow_sum(points) == fast_sum(points))
print('сумма квадратов:', fast_sum(points))
Вывод
результаты совпадают? True
сумма квадратов: 666667000

Результаты равны на любом движке — а вот соотношение скоростей меняется: на PyPy разница между неоптимальным циклом и генератором меньше, потому что JIT сам убирает часть накладных расходов. Это комплимент движку, но не замена голове: алгоритмическая экономия — с тысячами операций вместо миллионов — перекрывает ускорения от любой реализации.

Когда брать PyPy

Профиль работы решает всё. Долгоживущая программа на чистом Python — идеальный клиент PyPy: разогрев окупается на первых минутах. Сервис, обрабатывающий поток заказов, — тот же профиль: цикл живёт часами, и каждая процитированная итерация в машинном коде — экономия:

профиль долгоживущего обработчика
def process(orders):
    shipped = 0
    for order in orders:
        if order <= 0:
            continue
        shipped += 1
    return shipped

print('отправлено:', process([3, -1, 5, 0, 7]))
Вывод
отправлено: 3
  • брать PyPy: долгоживущие сервисы и обработчики потоков на чистом Python;
  • брать PyPy: числительные расчёты на чистом языке, где нет готовой библиотеки с машинным кодом;
  • не брать PyPy: одноразовые скрипты на секунды — разогрев не успеет окупиться;
  • не брать PyPy: код, плотно связанный с C-расширениями через C API;
  • не брать PyPy: встраивание в чужие приложения и работы, где нужен именно CPython-стек пакетов.

PyPy исполняет тот же язык, что и CPython: код, написанный на чистом Python, переезжает без единой правки, а выигрыш измеряется разами на горячих циклах. Цена — разогрев и ослабленная граница с C-кодом. Дальше по реализациям: MicroPython в уроке 18 выигрывает не скоростью, а размером — интерпретатор в сотни килобайт.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import sys
print('мы на PyPy?', sys.implementation.name == 'pypy')
def process(orders):
    shipped = 0
    for order in orders:
        if order <= 0:
            continue
        shipped += 1
    return shipped

print(process([3, -1, 5, 0, 7]))
total = 0
for i in range(1, 11):
    if i % 3 == 0 or i % 5 == 0:
        total += i
print(total)
Проверь себя
0 / 7

1. Что делает JIT-компиляция PyPy?

2. Почему первый прогон функции на PyPy медленнее последующих?

3. На каком коде ускорение от PyPy самое большое?

4. Как код узнаёт, что он запущен на PyPy?

5. Что рекомендует экосистема PyPy для вызова библиотек на C?

6. Что переезжает с CPython на PyPy без единой правки?

7. Когда PyPy проигрывает CPython?

Карточки терминов
Запомнено: 0 / 7
Практика

Посчитайте сумму чётных чисел от 1 до 100000 циклом и напечатайте результат одним числом. Результат должен быть одинаков на CPython и PyPy.

practice.py
Вопросы и ответы по уроку

Во сколько раз PyPy быстрее CPython?

На горячих циклах чистого Python — в разы, от двух-трёх до десятков в зависимости от задачи. На коде, где вся работа в C-расширениях, выигрыша нет, а на коротких скриптах PyPy может оказаться медленнее из-за разогрева.

Урок 16: реализации Python

Работает ли numpy на PyPy?

Numpy держит вычисления в машинном коде через C API, поэтому ускорения от JIT он не получает, а режим совместимости с C API замедляет вызовы. Для PyPy экосистема рекомендует cffi-подход к библиотекам на C или чистый Python.

Можно ли запускать обычные pip-пакеты на PyPy?

Да, через свой интерпретатор: pypy -m pip install пакет. У PyPy отдельная среда пакетов, а бинарные сборки для CPython не работают — пакет должен быть чистым Python или иметь сборку для PyPy.

Чем PyPy отличается от Nuitka?

PyPy ускоряет во время работы: JIT наблюдает за реальным профилем и компилирует горячие участки. Nuitka компилирует всё заранее в C, без разогрева, но полученный exe — тема урока про Nuitka, а задача PyPy — скорость долгоживущих программ.

Урок 12: Nuitka

Нужно ли менять код, чтобы он работал на PyPy?

Для чистого Python — нет: язык и стандартная библиотека те же, результаты идентичны. Менять стоит только код, опирающийся на C-расширения или на CPython-детали вроде подсчёта ссылок.

Понравился урок? Сошлитесь на него

«PyPy исполняет тот же язык, что и CPython: код, написанный на чистом Python, переезжает без единой правки.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.