MEASUREMENT
bench/gil/shared_vs_owned.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
62 lines"""Третий заход: общий объект против по-настоящему собственного.
Прошлая версия этого замера была неверной, и это стоит записать: список
создавался как ["строка"] * 16, то есть все 16 элементов ссылались на ОДИН
строковый литерал, общий для обоих потоков через константы кода. «Собственных»
объектов в том тесте не было ни одного.
Здесь объекты для каждого потока собираются внутри потока и из значений,
вычисленных в рантайме, — интернирование литералов до них не дотягивается.
Проверка того, что объекты действительно разные, делается по id().
"""
import json, sys, threading, time
def make_private(seed, n=16):
# str(number) считается в рантайме, поэтому объекты разные в каждом потоке.
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
def bench(make_items, k, nthreads, reps=5):
times = []
ids = []
for _ in range(reps):
barrier = threading.Barrier(nthreads)
res = [0.0] * nthreads
def run(t):
items = make_items(t)
ids.append(id(items[0]))
barrier.wait() # старт одновременный, иначе меряется разъезд
t0 = time.perf_counter()
loop(items, k)
res[t] = time.perf_counter() - t0
ts = [threading.Thread(target=run, args=(i,)) for i in range(nthreads)]
for t in ts: t.start()
for t in ts: t.join()
times.append(max(res)) # столько заняла работа целиком
return min(times), len(set(ids))
K = 2_000_000
SHARED = make_private(0) # один набор на всех
out = {"version": sys.version.split()[0],
"gil": getattr(sys, "_is_gil_enabled", lambda: True)(),
"iterations_per_thread": K}
one, _ = bench(lambda t: SHARED, K, 1)
two_shared, uniq_shared = bench(lambda t: SHARED, K, 2)
two_priv, uniq_priv = bench(lambda t: make_private(t + 1), K, 2)
out["one_thread_s"] = one
out["two_threads_shared_s"] = two_shared
out["two_threads_private_s"] = two_priv
out["speedup_shared"] = (2 * one) / two_shared
out["speedup_private"] = (2 * one) / two_priv
out["distinct_objects_shared_case"] = uniq_shared # ожидается 1
out["distinct_objects_private_case"] = uniq_priv # ожидается 2 на прогон
print(json.dumps(out, ensure_ascii=False))