ЗАМЕР
bench/free-threading-races/bytecode.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/python/concurrency/free-threading-races
Запись прогона
Гонки без GIL: замеры для статьи
Проверяется утверждение: код, который в сборке с GIL годами работал правильно, в свободной сборке ломается — хотя гонка в нём была всегда. GIL не делал операции атомарными; он лишь не давал потоку уступить управление в середине короткой последовательности инструкций.
Один и тот же код запускается на трёх интерпретаторах:
| сборка | что это |
|---|---|
3.14.10 |
обычная сборка, GIL включён |
3.14.10t |
свободная сборка той же версии, GIL выключен |
3.15.2t |
свободная сборка следующей версии: в ней исправлена гонка итератора range (cpython#129068) |
Как запустить
uv python install 3.14.10 3.14.10t 3.15.2t
./run.sh # записывает runs/<сборка>.txt
Нужен компилятор C (cc): gil_reenable.py собирает два крошечных
расширения из ext/ под тот интерпретатор, которым запущен.
| Скрипт | Что меряет |
|---|---|
bytecode.py |
инструкции между чтением и записью в двух формах приращения и есть ли среди них точка, где поток может уступить GIL |
lost_update.py |
сколько приращений из 4 000 000 теряется: c.n += 1, d["n"] += 1, c.n = c.n + one(); два исправления и их цена |
check_then_act.py |
ленивый кеш «проверил — записал»: два отдельных показателя — лишние вызовы make() (extra make() calls) и ключи, для которых потоки получили разные объекты (keys split); setdefault и блокировка. Раскол считается по самим объектам: каждый полученный объект удерживается до конца опыта, id сравниваются после join всех потоков |
shared_iterator.py |
один итератор range или списка на четыре потока: повторы, пропуски, значения вне источника; очередь как исправление |
gil_reenable.py |
импорт расширения многофазной инициализации без слота Py_mod_gil включает GIL для всего процесса: предупреждение и ускорение до и после (однофазный модуль заявляет поддержку вызовом PyUnstable_Module_SetGIL; здесь не меряется) |
context_flags.py |
thread_inherit_context и context_aware_warnings: тот же код даёт другой результат |
runs/3.14.10t-old-flags.txt — context_flags.py в свободной сборке с
ключами -X thread_inherit_context=0 -X context_aware_warnings=0: так
возвращается поведение сборки с GIL.
Машина замеров
2 vCPU (Intel Xeon; модель и частота — во второй строке каждой записи, текущие
записи сняты на 2.10GHz), Linux, интерпретаторы из python-build-standalone
(uv python install). Числа потерь от прогона к прогону разные — это и есть
содержание замера, поэтому печатаются все пять прогонов, а не среднее.
Времена — лучшее из пяти.
Скрипт
61 строк"""Байткод двух форм приращения: где у потока есть шанс уступить GIL.
В сборке с GIL поток отдаёт блокировку только в точках проверки
(`_CHECK_PERIODIC` в Python/bytecodes.c): на RESUME при входе в функцию,
после CALL, на JUMP_BACKWARD в конце тела цикла. Скрипт печатает инструкции
между чтением атрибута и записью в него и отмечает, есть ли среди них такая
точка. CALL помечен как точка проверки потому, что вызов её содержит: у
функции на C проверка выполняется в хвосте CALL, у функции на Python — в
RESUME на входе в неё (для one() срабатывает именно эта, и в этот список она
не попадает: она уже в теле вызываемой функции).
"""
import dis
from common import env_line, header
CHECKPOINTS = {"CALL", "RESUME", "JUMP_BACKWARD"}
class Box:
pass
def one():
return 1
def plus_equals(c):
c.n += 1
def call_between(c):
c.n = c.n + one()
def window(fn):
"""Инструкции от первого LOAD_ATTR n до STORE_ATTR n включительно."""
ins = [i for i in dis.get_instructions(fn)]
start = next(k for k, i in enumerate(ins) if i.opname == "LOAD_ATTR" and i.argval == "n")
end = next(k for k, i in enumerate(ins) if i.opname == "STORE_ATTR" and i.argval == "n")
return ins[start:end + 1]
def main():
print(env_line())
for n, title, fn in [
(1, "INCREMENT WITH NO CALL BETWEEN READ AND WRITE", plus_equals),
(2, "INCREMENT WITH A CALL BETWEEN READ AND WRITE", call_between),
]:
header(n, title)
span = window(fn)
for i in span:
mark = " <- checkpoint" if i.opname in CHECKPOINTS else ""
arg = f" {i.argrepr}" if i.argrepr else ""
print(f" {i.opname}{arg}{mark}")
hits = sum(1 for i in span if i.opname in CHECKPOINTS)
print(f"checkpoints between the read and the write: {hits}")
if __name__ == "__main__":
main()