Deep Engineering

ЗАМЕР

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

Скрипт

84 строк
"""Второй заход: цена свободных потоков и то, где они проигрывают."""
import json, os, sys, threading, time

REPS = 7

def spin(n):
    x = 0
    for i in range(n):
        x += i * i
    return x

def best(fn, *a, reps=REPS):
    t = []
    for _ in range(reps):
        t0 = time.perf_counter(); fn(*a); t.append(time.perf_counter() - t0)
    return min(t)

def single_thread_cost(n=6_000_000):
    """Однопоточная работа без единого потока — цена самой сборки."""
    return {"work": n, "seconds": best(spin, n)}

def shared_object_contention(iters=1_500_000):
    """Два потока трогают ОДИН объект. Каждое обращение меняет счётчик ссылок.

    В сборке с GIL счётчик обычный. В свободной сборке общий объект уходит
    на медленный путь подсчёта ссылок, и потоки начинают мешать друг другу.
    """
    shared = ["общий объект"] * 16

    def touch(k):
        s = shared
        x = 0
        for i in range(k):
            x += len(s[i & 15])
        return x

    one = best(lambda: touch(iters), reps=3)
    def two():
        ts = [threading.Thread(target=touch, args=(iters,)) for _ in range(2)]
        for t in ts: t.start()
        for t in ts: t.join()
    par = best(two, reps=3)
    return {"iterations": iters, "one_thread_s": one, "two_threads_s": par,
            "speedup": (2 * one) / par}

def private_object_scaling(iters=1_500_000):
    """То же самое, но у каждого потока свой список — общих объектов нет."""
    def touch(k):
        s = ["собственный объект"] * 16   # создаётся внутри потока
        x = 0
        for i in range(k):
            x += len(s[i & 15])
        return x
    one = best(lambda: touch(iters), reps=3)
    def two():
        ts = [threading.Thread(target=touch, args=(iters,)) for _ in range(2)]
        for t in ts: t.start()
        for t in ts: t.join()
    par = best(two, reps=3)
    return {"iterations": iters, "one_thread_s": one, "two_threads_s": par,
            "speedup": (2 * one) / par}

def gil_flags():
    out = {"PYTHON_GIL": os.environ.get("PYTHON_GIL")}
    try:
        out["_is_gil_enabled"] = sys._is_gil_enabled()
    except AttributeError:
        out["_is_gil_enabled"] = "нет такой функции"
    try:
        import sysconfig
        out["Py_GIL_DISABLED"] = sysconfig.get_config_var("Py_GIL_DISABLED")
    except Exception as e:
        out["Py_GIL_DISABLED"] = str(e)
    out["abiflags"] = getattr(sys, "abiflags", None)
    return out

print(json.dumps({
    "version": sys.version.split()[0],
    "flags": gil_flags(),
    "single_thread": single_thread_cost(),
    "shared": shared_object_contention(),
    "private": private_object_scaling(),
}, ensure_ascii=False))