Deep Engineering

ЗАМЕР

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

Скрипт

63 строк
"""Откуда берётся разница: один поток, но объект чужой.

Гипотеза, которую надо либо подтвердить, либо снять: в свободной сборке
объект «принадлежит» потоку, который его создал (biased reference counting,
PEP 703). Обращение из другого потока идёт по общему счётчику даже тогда,
когда никакой конкуренции нет — второго потока просто нет.

Проверка: ОДИН рабочий поток, два варианта данных.
  A. список создан в главном потоке, читается в рабочем — владелец чужой
  B. список создан в самом рабочем потоке — владелец он сам
Всё остальное одинаково.
"""
import json, sys, threading, time

K = 3_000_000
REPS = 7

def make(seed, n=16):
    return [("o" + str(seed * 1000 + i)) * 3 for i in range(n)]

def loop(items, k):
    x = 0
    for i in range(k):
        x += len(items[i & 15])
    return x

FOREIGN = make(0)          # создан в главном потоке

def one_worker(get_items):
    best = None
    for _ in range(REPS):
        res = {}
        def run():
            items = get_items()
            t0 = time.perf_counter()
            loop(items, K)
            res["t"] = time.perf_counter() - t0
        t = threading.Thread(target=run); t.start(); t.join()
        best = res["t"] if best is None else min(best, res["t"])
    return best

def in_main():
    best = None
    for _ in range(REPS):
        items = make(1)
        t0 = time.perf_counter(); loop(items, K); best2 = time.perf_counter() - t0
        best = best2 if best is None else min(best, best2)
    return best

foreign = one_worker(lambda: FOREIGN)
own = one_worker(lambda: make(2))
main = in_main()

print(json.dumps({
    "version": sys.version.split()[0],
    "gil": getattr(sys, "_is_gil_enabled", lambda: True)(),
    "iterations": K,
    "worker_reads_foreign_objects_s": foreign,
    "worker_reads_own_objects_s": own,
    "main_thread_reads_own_objects_s": main,
    "foreign_vs_own": foreign / own,
}, ensure_ascii=False))