Deep Engineering

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))