MEASUREMENT
bench/gil/interval_not_enforced.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/concurrency/gil
- How to run it
python3.13 bench/gil/measure.py python3.13t bench/gil/measure.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
Замеры для статьи «GIL: что это на самом деле»
Ничего не моделируется — только прогоны на живых интерпретаторах. Каждый
скрипт печатает JSON в поток вывода; summary.json — собранный вручную свод
тех прогонов, из которых взяты числа статьи.
Почему этот каталог появился поздно
Скрипты лежали в tmp/, а tmp/ стоит в .gitignore — то есть в репозитории
их не было вовсе, и числа статьи нельзя было перезапустить ни на другой машине,
ни на этой же через полгода. Правило «числа в статьях берутся отсюда, а не из
головы» для статей в этот момент было не выполнено, хотя для уроков
выполнялось. Перенос — исправление именно этого.
| Скрипт | Что меряет |
|---|---|
measure.py |
базовое: сборка, sys.getswitchinterval(), счёт в один и два потока, ожидание, zlib |
freethreading_cost.py |
цена свободной сборки на однопоточном коде и там, где она проигрывает |
shared_vs_owned.py |
общий объект против собственного; объекты собираются внутри потока и из значений времени выполнения |
owner_thread.py |
один рабочий поток, объект чужой против своего — проверка biased reference counting (PEP 703) |
interval_not_enforced.py |
«5 мс» — пожелание, а не гарантия: медиана ожидания против интервала на коротких и на длинных операциях |
timeline.py |
кто владел интерпретатором в каждый момент — данные для визуализации |
Запускать на каждом интерпретаторе отдельно:
python3.13 bench/gil/measure.py
python3.13t bench/gil/measure.py
Что этими замерами сравнивать нельзя
Общее правило в силе и здесь: время между версиями не сравнивается. Но у статьи про GIL есть собственная оговорка, и её надо держать в голове отдельно.
Строки вида «+14,4 %» получены сравнением двух сборок — обычной и
свободнопоточной. Формально это то же самое сравнение между сборками, которое
правило запрещает: подтвердить, что обе собраны одинаково и различаются ровно
--disable-gil, нечем — свободнопоточного интерпретатора на машине замеров
нет.
Поэтому такие проценты читаются как наблюдение на одной машине, а не как цена свободных потоков вообще. Именно так они и подписаны в тексте статьи. У официальной оценки («1–8 % на pyperformance») другое происхождение и другой набор нагрузок, и статья прямо говорит, что не опровергает её, а показывает худший случай.
Script
67 lines"""«5 мс» — это не гарантия, а пожелание. Проверка этого утверждения.
В ceval_gil.c рядом с DEFAULT_INTERVAL стоит комментарий: интервал
«encourages a defined switching period, but doesn't enforce it since opcodes
can take an arbitrary time to execute». Здесь это утверждение проверяется
измерением: если поток войдёт в ОДНУ операцию, которая считается долго,
интервал не поможет — прервать её негде.
"""
import json, statistics, sys, threading, time
def probe_lag(hog_body, duration=2.0, warmup=0.2):
stop = threading.Event()
lags = []
def hog():
while not stop.is_set():
hog_body()
def probe():
t_end = time.perf_counter() + duration
while not stop.is_set() and time.perf_counter() < t_end:
t0 = time.perf_counter()
time.sleep(0)
lags.append((time.perf_counter() - t0) * 1000)
h = threading.Thread(target=hog, daemon=True)
p = threading.Thread(target=probe)
h.start(); time.sleep(warmup); p.start()
p.join(); stop.set(); h.join(timeout=10)
lags.sort()
if not lags:
return None
return {"samples": len(lags),
"median_ms": round(statistics.median(lags), 3),
"p95_ms": round(lags[int(len(lags) * 0.95)], 3),
"max_ms": round(lags[-1], 3)}
# 1. Много коротких операций: интервал работает.
def many_small():
x = 0
for i in range(200_000):
x += i * i
# 2. Одна длинная операция. Умножение больших целых — это ОДИН вызов,
# внутри которого проверки eval breaker нет.
A = 7 ** 500_000
B = 11 ** 500_000
def one_big_mul():
A * B
# 3. Одна длинная операция другого рода: сортировка большого списка.
BIG = [(i * 2654435761) % 1_000_003 for i in range(2_000_000)]
def one_big_sort():
sorted(BIG)
out = {
"version": sys.version.split()[0],
"switch_interval_s": sys.getswitchinterval(),
"bits_A": A.bit_length(),
"bits_B": B.bit_length(),
"list_len": len(BIG),
"many_small_ops": probe_lag(many_small),
"one_big_int_multiply": probe_lag(one_big_mul),
"one_big_sort": probe_lag(one_big_sort),
}
print(json.dumps(out, ensure_ascii=False))