MEASUREMENT
bench/event-loop/runner.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/event-loop
- How to run it
python3.13 bench/event-loop/cost.py python3.13 bench/event-loop/runner.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
Замеры для статьи «Цикл событий»
| Скрипт | Что меряет |
|---|---|
cost.py |
из чего складывается цена await: обычный вызов, await без приостановки, создание объекта корутины, await завершённого Future, один оборот цикла |
runner.py |
во что обходится asyncio.run в цикле и что даёт переход на asyncio.Runner |
python3.13 bench/event-loop/cost.py
python3.13 bench/event-loop/runner.py
Почему этот каталог появился поздно и что он сразу поймал
Числа статьи считались наспех и в репозиторий не попали: скрипт лежал в tmp/,
а tmp/ стоит в .gitignore. Перезапустить их было нечем — и они разошлись с
уроком interview/python/async-await, где тот же обычный вызов стоил вдвое
дороже.
Разошлись не только абсолютные числа, но и кратности, а по общему правилу замеров кратности внутри одного запуска обязаны переживать смену машины. Перемер 21 августа 2026 подтвердил урок:
| было в статье | измерено | |
|---|---|---|
| обычный вызов | 20,3 нс | 44,6 нс |
await без приостановки |
56,6 нс (×2,8) | 90,8 нс (×2,0) |
| создать корутину и закрыть | 53,8 нс | 69,0 нс |
await asyncio.sleep(0) |
1698 нс (×83,7) | 2033 нс (×45,5) |
Заодно поехал вывод: «из 56,6 нс около 54 — создание объекта корутины» давало 95 %, а на самом деле это 76 %. Утверждение статьи от этого не изменилось — создание объекта всё равно составляет большую часть цены, — но подпирается теперь тем, что печатает скрипт.
Отдельно перемерен asyncio.run. В статье стояло «из ~53 мкс полезной работы
там 6» без источника; runner.py даёт 71 мкс на запуск против 0,12 мкс у той
же корутины внутри уже работающего цикла — то есть на цикл уходит 99,8 %.
Что этими замерами сравнивать нельзя
Общее правило замеров: время между версиями не сравнивается вообще. Колонки 3.13.7 и 3.14.7 в таблице статьи стоят рядом как два независимых результата; сравнивать можно только колонку кратностей, измеренную внутри одного запуска.
Script
72 lines"""Во сколько обходится сам `asyncio.run` — и сколько там полезной работы.
ЗАЧЕМ ЭТО МЕРИТЬ. `asyncio.run(main())` внутри цикла выглядит безобидно:
одна строка, вызывается корутина. На деле каждый вызов заводит новый цикл
событий, прогоняет корутину, а потом закрывает цикл — и закрытие само по себе
занимает несколько оборотов, потому что отменяются оставшиеся задачи и
дожидается `shutdown_asyncgens`.
Поэтому меряется три вещи, и смысл в их разнице:
1. `asyncio.run(noop())` — полная цена одного запуска;
2. та же корутина внутри уже работающего цикла — полезная работа;
3. `Runner`, переиспользующий один цикл, — то, чем `asyncio.run` в цикле
заменяется.
ОБЩЕЕ ПРАВИЛО ЗАМЕРОВ: время между версиями не сравнивается вообще.
Сравнивается только измеренное внутри одного запуска одного интерпретатора.
"""
import asyncio
import sys
import time
N = 2_000
REPEAT = 5
async def noop():
return 1
def best(fn, n=N, repeat=REPEAT):
"""Лучшее из `repeat` прогонов, микросекунд на итерацию."""
times = []
for _ in range(repeat):
start = time.perf_counter()
fn(n)
times.append(time.perf_counter() - start)
return min(times) / n * 1e6
def via_run(n):
for _ in range(n):
asyncio.run(noop())
def via_runner(n):
with asyncio.Runner() as runner:
for _ in range(n):
runner.run(noop())
def inside_one_loop(n):
async def body():
for _ in range(n):
await noop()
asyncio.run(body())
print("PY", sys.version.split()[0], f"| лучшее из {REPEAT} прогонов по {N:,} итераций".replace(",", " "))
run_us = best(via_run)
runner_us = best(via_runner)
inside_us = best(inside_one_loop)
print(f" asyncio.run(noop()) каждый раз {run_us:9.2f} мкс")
print(f" Runner, один цикл на все запуски {runner_us:9.2f} мкс")
print(f" await noop() внутри одного цикла {inside_us:9.4f} мкс <- полезная работа")
print(f" на заведение и закрытие цикла уходит {run_us - inside_us:9.2f} мкс "
f"({(run_us - inside_us) / run_us * 100:.1f} % от asyncio.run)")
print(f" переход на Runner дешевле asyncio.run в {run_us / runner_us:.1f} раза")