Deep Engineering

ЗАМЕР

bench/gil/timeline.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/python/concurrency/gil
Как запустить
python3.13  bench/gil/measure.py
python3.13t bench/gil/measure.py

Запись прогона

Замеры для статьи «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») другое происхождение и другой набор нагрузок, и статья прямо говорит, что не опровергает её, а показывает худший случай.

Скрипт

61 строк
"""Кто владел интерпретатором в каждый момент — данные для визуализации.

Каждый поток крутит счётчик и ставит отметку времени. Отметки двух потоков
сливаются в один отсортированный ряд; смена идентификатора в этом ряду —
момент, когда владение перешло. Точность ограничена шагом между отметками
(порядка десятков микросекунд), и этого достаточно, чтобы увидеть кванты
в 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))