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