MEASUREMENT
bench/gil/timeline.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
61 lines"""Кто владел интерпретатором в каждый момент — данные для визуализации.
Каждый поток крутит счётчик и ставит отметку времени. Отметки двух потоков
сливаются в один отсортированный ряд; смена идентификатора в этом ряду —
момент, когда владение перешло. Точность ограничена шагом между отметками
(порядка десятков микросекунд), и этого достаточно, чтобы увидеть кванты
в 5 мс.
"""
import json, sys, threading, time
def timeline(duration=0.25, nthreads=2, interval=None, granularity=1500):
if interval is not None:
sys.setswitchinterval(interval)
stop = threading.Event()
out = []
lock = threading.Lock()
def worker(tid):
local = []
x = 0
while not stop.is_set():
for _ in range(granularity):
x += 1
local.append((tid, time.perf_counter()))
with lock:
out.extend(local)
ts = [threading.Thread(target=worker, args=(i,)) for i in range(nthreads)]
t0 = time.perf_counter()
for t in ts: t.start()
time.sleep(duration)
stop.set()
for t in ts: t.join()
out.sort(key=lambda m: m[1])
# Сжимаем в отрезки владения.
segs = []
cur_tid, cur_start, prev = out[0][0], out[0][1], out[0][1]
for tid, t in out[1:]:
if tid != cur_tid:
segs.append({"thread": cur_tid,
"start_ms": round((cur_start - t0) * 1000, 3),
"end_ms": round((prev - t0) * 1000, 3)})
cur_tid, cur_start = tid, t
prev = t
segs.append({"thread": cur_tid,
"start_ms": round((cur_start - t0) * 1000, 3),
"end_ms": round((prev - t0) * 1000, 3)})
durs = sorted(s["end_ms"] - s["start_ms"] for s in segs)
return {"switch_interval_s": sys.getswitchinterval(),
"duration_ms": round((time.perf_counter() - t0) * 1000, 2),
"marks": len(out), "segments": len(segs),
"median_segment_ms": durs[len(durs) // 2],
"segments_data": segs}
res = {"version": sys.version.split()[0],
"gil": getattr(sys, "_is_gil_enabled", lambda: True)()}
res["default_5ms"] = timeline(interval=0.005)
res["fine_0_5ms"] = timeline(interval=0.0005)
print(json.dumps(res, ensure_ascii=False))