ЗАМЕР
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))