Python 3.14 и его новый JIT-компилятор
Технический обзор и некоторые сравнительные показатели.
Делиться
Выпуск Python 3.14 знаменует собой важный этап в эволюции самого популярного в мире языка программирования. Хотя Python давно известен своей читаемостью и обширной экосистемой, скорость его выполнения часто оставалась «слоном в комнате».
С выходом версии 3.14 команда разработчиков ядра CPython представила не одну, а две самые ожидаемые функции за последнее время.
Конец GIL
Я уже писал об этом раньше. Теперь в Python доступна настоящая параллельная обработка, если она вам нужна. Если вы хотите узнать больше о Python без GIL, я оставлю ссылку на свою статью об этом в конце.
Компилятор Just-In-Time (JIT)
Эта экспериментальная функция теперь включена непосредственно в официальные установщики, и именно на ней мы сосредоточимся здесь. Это результат многолетней архитектурной подготовки, проведенной основной командой Python и другими, направленной на то, чтобы сделать Python «быстрее по умолчанию», не нарушая экосистему расширений C, которая лежит в основе всего, от анализа данных до веб-бэкендов.
В этой статье мы заглянем под капот новой системы JIT, рассмотрим, чем она отличается от предыдущих методов оптимизации, и ознакомимся с некоторыми методами бенчмаркинга, которые помогут вам решить, пора ли попробовать JIT на ваших рабочих нагрузках.
Что такое новый JIT-компилятор (Just-In-Time) в Python?
Чтобы понять принцип работы JIT-компилятора версии 3.14, нам нужно разобраться, как традиционно работает Python. Стандартный Python (CPython) — это интерпретируемый язык. При запуске скрипта ваш код компилируется в байт-код, который представляет собой набор инструкций, выполняемых виртуальной машиной CPython.
JIT-компилятор меняет этот процесс. Вместо того чтобы просто построчно интерпретировать байт-код, JIT-компилятор отслеживает, какие части вашего кода выполняются наиболее часто («горячие» пути). Когда функция или цикл считаются «горячими», JIT-компилятор преобразует байт-код в машинный код (инструкции, понятные процессору). Затем, при следующем вызове кода, интерпретация не требуется. Вместо этого он просто выполняется как есть. Это может значительно сэкономить время, как мы увидим позже.
Как JIT-компилятор интегрируется в CPython
JIT-компилятор Python 3.14 не является полной переработкой кода. Он разработан как компонент, который можно включить по желанию и который работает параллельно с существующим интерпретатором. В нем используется метод «копирования и внесения изменений», который позволяет JIT-компилятору быть легковесным и переносимым на различные архитектуры ЦП без необходимости использования громоздкого и сложного бэкенда компилятора, такого как LLVM.
Что изменилось в Python 3.14?
В Python 3.13 существовал базовый экспериментальный JIT-компилятор, но он был отключен по умолчанию. Для его тестирования необходимо было клонировать исходный код CPython и скомпилировать его со специальными экспериментальными флагами, такими как - - enable-experimental-jit .
С выходом Python 3.14 все изменилось. JIT-компилятор стал доступен в официальных установщиках .msi (Windows) и .pkg (macOS). Это также означало, что для использования преимуществ JIT-компиляции больше не требовался компилятор C на вашем компьютере. Хотя это все еще «экспериментальная» функция, ее включение в официальные бинарные файлы свидетельствует о том, что основная команда считает JIT-компилятор достаточно стабильным для широкого тестирования сообществом.
Установка Python 3.14
Перейдите по ссылке https://www.python.org/downloads/, и вы увидите опцию загрузки версии 3.14. Нажмите на неё, а затем следуйте инструкциям.
В качестве альтернативы, если у вас установлен инструмент UV , вы можете ввести следующее.
PS C: > uv python install 3.14
Включение системы «точно в срок»
По умолчанию JIT-компилятор отключен . Это мера безопасности; поскольку он является экспериментальным, Совет разработчиков Python хочет гарантировать, что пользователи не столкнутся с неожиданными ухудшениями стабильности или снижением использования памяти без явного выбора этой опции.
Для активации JIT-компилятора используется переменная среды. Она указывает среде выполнения CPython инициализировать JIT-компилятор при запуске.
В Windows (PowerShell):
$env:PYTHON_JIT=1 python my_script.py
В macOS/Linux (Bash/Zsh):
PYTHON_JIT=1 python my_script.py
После включения CPython не выполняет JIT-компиляцию всего кода сразу. Он использует систему многоуровневого компиляции. По сути, он сначала пытается выполнить код с наименьшими затратами ресурсов, и тратит усилия на компиляцию/оптимизацию только на те части, которые оказываются наиболее ресурсоемкими.
- Уровень 0: Стандартная интерпретация.
- Уровень 1: Специализированный байт-код (введен в версии 3.11).
- Уровень 2 (JIT): Генерация машинного кода для наиболее часто используемых путей.
Оценка эффективности системы «точно в срок»
При тестировании JIT-компилятора нельзя просто использовать ` time.time()` вокруг функции. JIT-компиляторам требуется период прогрева . Первые несколько итераций цикла могут быть медленнее обычного, пока JIT-компилятор профилирует код, но последующие итерации могут быть значительно быстрее.
Набор инструментов для сравнительного анализа производительности
Ниже представлен полный набор тестов, предназначенный для проверки различных аспектов системы JIT (Just-in-Time — «точно в срок»), от сложных математических вычислений до манипуляций со сложными объектами.
Файл 1: workloads.py
Этот файл содержит три различные задачи, интенсивно использующие ресурсы процессора.
1/ Функция Мандельброта итерирует формулу Мандельброта по сетке пикселей и возвращает контрольную сумму количества итераций для каждого пикселя.
2/ Функция Дейкстры строит детерминированный случайный взвешенный граф и запускает алгоритм Дейкстры, начиная с узла 0, возвращая количество завершенных/посещенных узлов.
3/ Функция Левенштейна генерирует N детерминированных случайных пар строк и возвращает сумму их расстояний Левенштейна.
from __future__ import annotations import random import heapq # Workload 1: Mandelbrot (CPU + math loops) def mandelbrot(width: int = 1000, height: int = 1000, iters: int = 500) -> int: checksum = 0 for y in range(height): cy = (y / height) * 2.4 - 1.2 for x in range(width): cx = (x / width) * 3.2 - 2.2 zx, zy, count = 0.0, 0.0, 0 while zx * zx + zy * zy <= 4.0 and count < iters: zx, zy = zx * zx - zy * zy + cx, 2.0 * zx * zy + cy count += 1 checksum += count return checksum # Workload 2: Dijkstra (heap + list + logic) def dijkstra(n: int = 10000, edges_per_node: int = 50, seed: int = 123) -> int: rng = random.Random(seed) graph = [[] for _ in range(n)] for u in range(n): for _ in range(edges_per_node): v = rng.randrange(n) if v != u: graph[u].append((v, rng.randrange(1, 30))) dist = [10**12] * n dist[0] = 0 pq = [(0, 0)] visited = 0 while pq: d, u = heapq.heappop(pq) if d != dist[u]: continue visited += 1 for v, w in graph[u]: nd = d + w if nd < dist[v]: dist[v] = nd heapq.heappush(pq, (nd, v)) return visited # Workload 3: Levenshtein distance (dynamic programming) def levenshtein(a: str, b: str) -> int: prev = list(range(len(b) + 1)) for i, ca in enumerate(a, 1): cur = [i] for j, cb in enumerate(b, 1): cur.append(min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + (ca != cb))) prev = cur return prev[-1] def levenshtein_batch(n: int = 10000, seed: int = 7, k: int = 50) -> int: """ Deterministic batch: fixed RNG seed, fixed alphabet, fixed string length. Returns the sum of distances. """ rng = random.Random(seed) alphabet = "abc" total = 0 for _ in range(n): a = "".join(rng.choices(alphabet, k=k)) b = "".join(rng.choices(alphabet, k=k)) total += levenshtein(a, b) return total
Файл 2: benchmark.py
Этот скрипт автоматизирует сравнение различных рабочих нагрузок с включенной и выключенной функцией JIT-компиляции.
import os import time import json import subprocess from pathlib import Path PYTHON_EXE = r"C:UsersthomaAppDataLocalProgramsPythonPython314python.exe" PROJECT_DIR = Path(__file__).resolve().parent # Original workloads (statement prints a result for sanity) WORKLOADS = [ ("mandelbrot", 'from workloads import mandelbrot; print(mandelbrot())'), ("dijkstra", 'from workloads import dijkstra; print(dijkstra())'), ("levenshtein_batch", 'from workloads import levenshtein_batch; print(levenshtein_batch())'), ] N_RUNS = 10 # average of ALL runs (set to 6/10/20 as you like) OUTFILE = PROJECT_DIR / "results_avg.json" def run_once(stmt: str, jit_val: int) -> tuple[float, str]: env = os.environ.copy() env["PYTHON_JIT"] = str(jit_val) # Ensure local workloads.py is importable in subprocess env["PYTHONPATH"] = str(PROJECT_DIR) + (os.pathsep + env.get("PYTHONPATH", "")) t0 = time.perf_counter() p = subprocess.run( [PYTHON_EXE, "-c", stmt], env=env, cwd=str(PROJECT_DIR), capture_output=True, text=True, ) t1 = time.perf_counter() if p.returncode != 0: raise RuntimeError( f"Run failed (PYTHON_JIT={jit_val})nn" f"Statement:n{stmt}nn" f"STDOUT:n{p.stdout}nnSTDERR:n{p.stderr}" ) return (t1 - t0, p.stdout.strip()) def summarize(times: list[float]) -> dict: return { "avg": sum(times) / len(times), "min": min(times), "max": max(times), "runs": times, } def bench_workload(name: str, stmt: str) -> dict: results = {} outputs = {} for jit_val in (0, 1): times = [] outs = [] print(f" PYTHON_JIT={jit_val}: running {N_RUNS} times...") for i in range(1, N_RUNS + 1): dt, out = run_once(stmt, jit_val) times.append(dt) outs.append(out) print(f" run {i}/{N_RUNS}: {dt:.6f}s") results[jit_val] = summarize(times) outputs[jit_val] = outs avg0 = results[0]["avg"] avg1 = results[1]["avg"] speedup = avg0 / avg1 if avg1 else float("inf") delta_pct = (avg1 - avg0) / avg0 * 100.0 if avg0 else 0.0 return { "workload": name, "jit0": results[0], "jit1": results[1], "speedup_jit0_over_jit1": speedup, "delta_pct_jit1_vs_jit0": delta_pct, "outputs": outputs, # sanity: should be stable } def main() -> int: all_results = [] print(f"Using Python: {PYTHON_EXE}") print(f"Project dir: {PROJECT_DIR}") print(f"Runs per setting (avg of all runs): {N_RUNS}n") for name, stmt in WORKLOADS: print(f"=== {name} ===") r = bench_workload(name, stmt) all_results.append(r) print(f"n Averages:") print(f" JIT=0 avg: {r['jit0']['avg']:.6f}s (min {r['jit0']['min']:.6f}, max {r['jit0']['max']:.6f})") print(f" JIT=1 avg: {r['jit1']['avg']:.6f}s (min {r['jit1']['min']:.6f}, max {r['jit1']['max']:.6f})") print(f" Speedup (JIT=0 / JIT=1): {r['speedup_jit0_over_jit1']:.3f}× (Δ={r['delta_pct_jit1_vs_jit0']:+.2f}%)n") # Optional: warn if outputs vary across runs (nondeterminism) if len(set(r["outputs"][0])) != 1: print(" !! WARNING: JIT=0 output differs across runs (nondeterministic workload?)") if len(set(r["outputs"][1])) != 1: print(" !! WARNING: JIT=1 output differs across runs (nondeterministic workload?)") OUTFILE.write_text(json.dumps(all_results, indent=2), encoding="utf-8") print(f"Wrote: {OUTFILE}") return 0 if __name__ == "__main__": raise SystemExit(main())
Вот мои результаты.
C:Usersthomaprojectspython_jit>C:UsersthomaAppDataLocalProgramsPythonPython314python.exe benchmark.py Using Python: C:UsersthomaAppDataLocalProgramsPythonPython314python.exe Project dir: C:Usersthomaprojectspython_jit Runs per setting (avg of all runs): 10 === mandelbrot === PYTHON_JIT=0: running 10 times... run 1/10: 6.890924s run 2/10: 6.950737s run 3/10: 7.265357s run 4/10: 6.947150s run 5/10: 6.932333s run 6/10: 6.939378s run 7/10: 7.194705s run 8/10: 6.995550s run 9/10: 6.902696s run 10/10: 7.256164s PYTHON_JIT=1: running 10 times... run 1/10: 5.216740s run 2/10: 5.241888s run 3/10: 5.350822s run 4/10: 5.246767s run 5/10: 5.294771s run 6/10: 5.273295s run 7/10: 5.272135s run 8/10: 5.617062s run 9/10: 5.251656s run 10/10: 5.239060s Averages: JIT=0 avg: 7.027499s (min 6.890924, max 7.265357) JIT=1 avg: 5.300420s (min 5.216740, max 5.617062) Speedup (JIT=0 / JIT=1): 1.326× (Δ=-24.58%) === dijkstra === PYTHON_JIT=0: running 10 times... run 1/10: 0.235401s run 2/10: 0.227603s run 3/10: 0.244492s run 4/10: 0.232971s run 5/10: 0.249589s run 6/10: 0.232229s run 7/10: 0.229422s run 8/10: 0.238399s run 9/10: 0.230657s run 10/10: 0.235772s PYTHON_JIT=1: running 10 times... run 1/10: 0.238862s run 2/10: 0.239266s run 3/10: 0.240312s run 4/10: 0.231413s run 5/10: 0.232692s run 6/10: 0.233783s run 7/10: 0.230016s run 8/10: 0.237760s run 9/10: 0.240895s run 10/10: 0.246033s Averages: JIT=0 avg: 0.235653s (min 0.227603, max 0.249589) JIT=1 avg: 0.237103s (min 0.230016, max 0.246033) Speedup (JIT=0 / JIT=1): 0.994× (Δ=+0.62%) === levenshtein_batch === PYTHON_JIT=0: running 10 times... run 1/10: 2.176256s run 2/10: 2.171253s run 3/10: 2.171834s run 4/10: 2.170444s run 5/10: 2.149874s run 6/10: 2.162820s run 7/10: 2.171975s run 8/10: 2.199151s run 9/10: 2.168398s run 10/10: 2.167821s PYTHON_JIT=1: running 10 times... run 1/10: 1.575666s run 2/10: 1.612615s run 3/10: 1.571106s run 4/10: 1.584650s run 5/10: 1.579948s run 6/10: 1.582633s run 7/10: 1.593924s run 8/10: 1.573608s run 9/10: 1.581427s run 10/10: 1.578553s Averages: JIT=0 avg: 2.170983s (min 2.149874, max 2.199151) JIT=1 avg: 1.583413s (min 1.571106, max 1.612615) Speedup (JIT=0 / JIT=1): 1.371× (Δ=-27.06%)
Интерпретация результатов
Как видите, результаты неоднозначны. Это нормально для экспериментальной системы «точно в срок».
- Ускорение на 10–30%: часто встречается в циклах «чистого Python» (например, в тестах Мандельброта или Левенштейна), где JIT-компилятор может избежать накладных расходов цикла диспетчеризации байт-кода.
- Улучшение на 0%: характерно для задач, интенсивно использующих ввод-вывод, или кода, активно использующего расширения C. Код Дейкстры не ускорился, потому что его время выполнения в основном определяется операциями с кучей/кортежами и ресурсоемкими операциями выделения памяти, которые текущий JIT-компилятор CPython не оптимизирует в значительной степени, поэтому любая экономия ресурсов интерпретатора теряется в общем объеме кода.
Когда использовать JIT-компиляцию в Python 3.14?
Система JIT (точно в срок) — мощный инструмент, но это не «волшебная кнопка». Из моего опыта, систему JIT следует использовать, когда у вас есть…
- Логика, ресурсоемкая для ЦП: Ваше приложение выполняет сложные вычисления, обработку данных или сложную логику на чистом Python.
- Длительно выполняющиеся процессы: веб-серверы (Gunicorn/Uvicorn) или фоновые рабочие процессы (Celery), работающие в течение нескольких часов, что дает JIT-компилятору достаточно времени для прогрева и оптимизации ресурсоемких процессов.
- Экспериментальное тестирование: Вам необходимо подготовить свой код к будущим версиям Python (3.15+), где JIT-компилятор, вероятно, будет работать более агрессивно.
И избегайте этого, если у вас есть…
- Приложения, сильно зависящие от операций ввода-вывода : если ваше приложение просто ожидает запросов к базе данных или ответов от API, JIT-компилятор не поможет.
- В средах с ограниченными ресурсами памяти: небольшие лямбда-функции или крошечные контейнеры могут страдать от увеличения объема памяти, занимаемого JIT-кэшем.
- Инструменты командной строки с коротким сроком службы: скрипту, выполняющемуся менее чем за секунду, не нужен JIT-компилятор.
Перспективы на будущее: за пределами 3.14
Основная команда разработчиков CPython рассматривает версию 3.14 как «год закладки фундамента». Ожидается, что будущие итерации (Python 3.15 и 3.16) будут включать в себя:
- Более глубокие этапы оптимизации: использование информации о типах, собранной во время выполнения, для еще более агрессивной генерации машинного кода.
- Улучшенная эвристика: более разумные решения о времени компиляции, снижающие затраты времени на «прогрев».
- Снижение накладных расходов: усовершенствование механизма копирования и внесения изменений для уменьшения потребления памяти.
Краткое содержание
JIT-компилятор в Python 3.14 — это не просто патч для повышения производительности. Это заявление о намерениях. Он показывает, что Python всерьез намерен сократить разрыв в производительности с такими языками, как Java или Go, сохраняя при этом простоту «с батарейками», которая сделала его знаменитым.
Для большинства разработчиков JIT-компилятор — это просто ещё один инструмент, за которым стоит следить. Если производительность важна в ваших проектах, стоит протестировать Python 3.14 на существующих рабочих нагрузках. Несколько тестов на наиболее важных участках кода могут выявить прирост производительности там, где вы его не ожидали.
Вот ссылка на мою предыдущую статью о GIL Fee Python, о которой я упоминал в начале.
Python 3.14 и конец GIL
Томас Рид. Все материалы от Томаса Рида.
Источник: towardsdatascience.com
Похожие записи
Оцените материал:
Похожие записи
Границы применимости LLM в мобильном UI-дизайне
20.06.2026
Надвигающееся подавление общения с искусственным интеллектом
26.09.2025
Вопросы и ответы: MIT SHASS и будущее образования в эпоху искусственного интеллекта
16.04.2026Присоединяйтесь и подпишитесь на рассылку самых свежих новостей по Email
Получайте свежие новости и идеи на почту. Без спама — только самое интересное.
Нажимая «Подписаться», вы соглашаетесь с политикой конфиденциальности.
