MEASUREMENT
bench/adaptive/jit.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/python/runtime/adaptive-interpreter
- How to run it
python3.11 bench/adaptive/specialize.py python3.12 bench/adaptive/specialize.py python3.13 bench/adaptive/specialize.py python3.14 bench/adaptive/specialize.py python3.13 bench/adaptive/cost.py python3.14 bench/adaptive/cost.py python3.14 bench/adaptive/jit.py PYTHON_JIT=1 python3.14 bench/adaptive/jit.py
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для статьи «Адаптивный интерпретатор»
| Скрипт | Что делает |
|---|---|
specialize.py |
карта семейств инструкций, что во что превращается после прогрева, на каком выполнении это случается, что происходит при смене типа |
cost.py |
цена места вызова, через которое проходит один тип против пяти: одна и та же работа, различается только полиморфизм |
jit.py |
второй уровень включён и выключен на ОДНОЙ сборке — четыре коротких цикла |
python3.11 bench/adaptive/specialize.py
python3.12 bench/adaptive/specialize.py
python3.13 bench/adaptive/specialize.py
python3.14 bench/adaptive/specialize.py
python3.13 bench/adaptive/cost.py
python3.14 bench/adaptive/cost.py
python3.14 bench/adaptive/jit.py
PYTHON_JIT=1 python3.14 bench/adaptive/jit.py
Что здесь важно прочитать правильно
specialize.py не меряет время. Он печатает то, что интерпретатор
сообщает о себе сам: имя инструкции до прогрева и после, размер семейства,
номер выполнения, на котором подстановка случилась. Такие утверждения между
версиями сравнимы — это счёт, а не секунды, — и прогон по четырём версиям
сделан именно ради сравнения.
cost.py сравнивает только строки внутри своего прогона. Пять классов
объявлены одинаково и делают одно и то же; различается лишь число типов,
проходящих через одну строку o.x. Чтение идёт внутри цикла, а не через вызов
на каждый элемент: иначе разница утонула бы в цене самого вызова.
jit.py — единственное место, где сравнение по времени законно между
запусками. Общее правило замеров запрещает сравнивать РАЗНЫЕ сборки; здесь
сборка одна, и различается ровно одна переменная окружения PYTHON_JIT. Это
контролируемый опыт, а не сравнение версий.
Запускать чередующимися парами, а не один раз. Машина дрейфует: один и тот же режим от запуска к запуску различается на несколько процентов, и на одной паре разница в четыре процента не значит ничего.
for i in 1 2 3 4 5 6; do
python3.14 bench/adaptive/jit.py --tsv
PYTHON_JIT=1 python3.14 bench/adaptive/jit.py --tsv
done
Критерий — пересечение размахов, а не порог. Шесть минимумов одного режима дают отрезок, шесть минимумов другого — второй отрезок. Если отрезки не пересекаются, разница есть; если пересекаются — сказать нечего. Порог пришлось бы выбирать, и выбор был бы подгонкой под эти четыре строки.
Что получилось на 3.14.7, шесть чередующихся пар (мкс на вызов hot()):
| нагрузка | без JIT | с JIT | вывод |
|---|---|---|---|
| счётный цикл | 981–1013 | 1144–1216 | +16,6 %, не пересекаются |
| цикл по списку с атрибутом | 520–541 | 711–739 | +36,7 %, не пересекаются |
| вызов функции в цикле | 1018–1037 | 1169–1199 | +14,8 %, не пересекаются |
| словарь в цикле | 1520–1588 | 1683–1746 | +10,7 %, не пересекаются |
Здесь же — почему пар шесть, а не три, как сказано выше. Прежняя редакция таблицы снималась на 3.14.0rc2 и давала пёструю картину: на вызове функции второй уровень выигрывал (−7,5 %), на счётном цикле и обходе списка проигрывал, на словаре размахи пересекались. На финальной 3.14.7 выигрыша нет ни на одной нагрузке, и размахи не пересекаются нигде. Когда меняется не число, а вывод, трёх пар мало.
Старые числа не приписаны рядом и не сохранены для сравнения намеренно: rc2
собран Clang 20.1.4, а 3.14.7 — Clang 22.1.3, и ставить их времена в одну
таблицу запрещает правило из bench/README.md: время между версиями не
сравнивается.
Что этими числами доказать НЕЛЬЗЯ
Приговор второму уровню. Здесь четыре коротких цикла на двух ядрах; ни
pyperformance, ни настоящей программы тут нет. Право эти числа дают ровно на
одно утверждение: включение второго уровня само по себе выигрыша не гарантирует
— на всех четырёх нагрузках он проиграл.
Script
115 lines"""Второй уровень: даёт ли он что-нибудь на этой машине.
ЗАЧЕМ. Про JIT в 3.13–3.14 пишут много, и почти всё — пересказ анонсов. Здесь
единственный вопрос, на который можно ответить своими руками: включение второго
уровня на ТОЙ ЖЕ сборке меняет время или нет.
ПОЧЕМУ ЭТО СРАВНЕНИЕ ЗАКОННО, хотя общее правило замеров запрещает сравнивать
версии по времени. Правило запрещает сравнивать РАЗНЫЕ СБОРКИ: у них разный
компилятор и разные флаги. Здесь сборка одна и та же, и различается ровно одна
переменная окружения — `PYTHON_JIT`. Это и есть контролируемый опыт.
КАК ЗАПУСКАТЬ. Не дважды, а несколькими ЧЕРЕДУЮЩИМИСЯ парами: машина дрейфует,
и один и тот же режим от запуска к запуску различается на 8–17 %. Чередование
нужно, чтобы дрейф попал в оба режима одинаково.
for i in 1 2 3; do
python3.14 bench/adaptive/jit.py --tsv
PYTHON_JIT=1 python3.14 bench/adaptive/jit.py --tsv
done | sort
Сравнивать надо не средние, а МИНИМУМЫ по рангам: три минимума одного режима
против трёх минимумов другого, первый с первым, второй со вторым. Если знак
разницы одинаков во всех трёх позициях — это уже не дрейф.
ЧТО НЕЛЬЗЯ ИЗ ЭТОГО ВЫВОДИТЬ. Приговор JIT вообще. Здесь четыре коротких цикла
на одной машине; ни pyperformance, ни настоящей программы тут нет. Вывод, на
который эти числа дают право, ровно один: на таком коде и на этой машине
разницы либо нет, либо она не в пользу второго уровня.
"""
import sys
import timeit
REPEAT = 11
def jit_status() -> str:
jit = getattr(sys, "_jit", None)
if jit is None:
return "второго уровня в этой сборке нет"
parts = []
for name in ("is_available", "is_enabled", "is_active"):
fn = getattr(jit, name, None)
if fn is not None:
parts.append(f"{name}={fn()}")
return ", ".join(parts)
WORKLOADS = {
"счётный цикл (i*i)": (
"def hot(n):\n t = 0\n for i in range(n):\n t += i * i\n return t\n",
"hot(20000)",
),
"цикл по списку с атрибутом": (
"class P:\n"
" def __init__(self):\n"
" self.x = 1\n"
"OBJ = [P() for _ in range(20000)]\n"
"def hot(seq):\n"
" t = 0\n"
" for o in seq:\n"
" t += o.x\n"
" return t\n",
"hot(OBJ)",
),
"вызов функции в цикле": (
"def inner(a):\n return a + 1\n"
"def hot(n):\n"
" t = 0\n"
" for i in range(n):\n"
" t = inner(t)\n"
" return t\n",
"hot(20000)",
),
"словарь в цикле": (
"D = {i: i for i in range(1000)}\n"
"def hot(n):\n"
" t = 0\n"
" for i in range(n):\n"
" t += D[i % 1000]\n"
" return t\n",
"hot(20000)",
),
}
def main() -> None:
tsv = "--tsv" in sys.argv
mode = "on" if getattr(sys, "_jit", None) and sys._jit.is_enabled() else "off"
if tsv:
for label, (setup, stmt) in WORKLOADS.items():
ns: dict = {}
exec(setup, ns)
times = timeit.repeat(stmt, globals=ns, repeat=REPEAT, number=50)
print(f"{mode}\t{label}\t{min(times) / 50 * 1e6:.1f}")
return
print("PY", sys.version.split()[0])
print(" JIT:", jit_status())
print(f" лучшее из {REPEAT} прогонов, микросекунды на один вызов hot()")
print()
for label, (setup, stmt) in WORKLOADS.items():
ns: dict = {}
exec(setup, ns)
times = timeit.repeat(stmt, globals=ns, repeat=REPEAT, number=50)
best = min(times) / 50 * 1e6
spread = (max(times) - min(times)) / min(times) * 100
print(f" {label:<28} {best:8.1f} мкс разброс по прогонам {spread:4.1f} %")
if __name__ == "__main__":
main()