MEASUREMENT
bench/event-loop/cost.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
129 lines"""Из чего складывается цена `await` — и где начинается цикл событий.
ЗАЧЕМ ЭТО МЕРИТЬ. Про `await` говорят «это переключение контекста, его надо
экономить». Замер показывает, что переключения там чаще всего нет вовсе:
пока по пути не встретился Future, который действительно приостанавливает,
цепочка `await` схлопывается внутри интерпретатора и до цикла не доходит.
Поэтому строк здесь пять, и они выстроены так, чтобы разложить цену на части:
1. обычный вызов — точка отсчёта;
2. `await` корутины, которая ничего не ждёт;
3. создать объект корутины и закрыть, БЕЗ `await` — если эта строка близка
к предыдущей, значит платят не за `await`, а за создание объекта;
4. `await` уже завершённого Future — первый заход в машинерию задач;
5. `await asyncio.sleep(0)` — ровно один полный оборот цикла.
ЧТО ЗДЕСЬ НЕЛЬЗЯ ПРОЧИТАТЬ. Общее правило замеров: время между версиями
не сравнивается вообще. Сравнивается только измеренное внутри одного запуска
одного интерпретатора — то есть колонка кратностей. Числа 3.13.7 и 3.14.0rc2
приводятся рядом как два независимых результата, а не как сравнение.
ПОЧЕМУ ЭТОТ СКРИПТ ПОЯВИЛСЯ ПОЗЖЕ СТАТЬИ. Числа статьи про цикл событий
считались наспех и в репозиторий не попали: скрипт лежал в `tmp/`, а `tmp/`
в `.gitignore`. Перезапустить их было нечем, и они разошлись с уроком про
`async/await`, где тот же обычный вызов стоил вдвое дороже. Разошлись не
только абсолютные числа, но и кратности — а кратности по правилу выше обязаны
переживать смену машины. Скрипт написан ровно затем, чтобы такое ловилось
запуском, а не вычиткой.
"""
import asyncio
import sys
import time
N = 200_000
REPEAT = 7
def plain(x):
return x + 1
async def coro(x):
return x + 1
async def bench_plain(n):
for i in range(n):
plain(i)
async def bench_await(n):
for i in range(n):
await coro(i)
async def bench_create_close(n):
"""Создать объект корутины и закрыть, ни разу не сделав `await`.
Без `close()` интерпретатор на каждой итерации печатал бы
«coroutine was never awaited» — и мерилось бы уже не создание объекта,
а работа предупреждающего механизма.
"""
for i in range(n):
c = coro(i)
c.close()
async def bench_done_future(n):
"""`await` футуры, у которой результат уже есть.
Приостановки здесь нет: `Future.__await__` при `done()` отдаёт результат
сразу. Зато есть весь остальной путь — создание футуры и заход в её
протокол ожидания.
"""
loop = asyncio.get_running_loop()
for _ in range(n):
fut = loop.create_future()
fut.set_result(1)
await fut
async def bench_sleep0(n):
for _ in range(n):
await asyncio.sleep(0)
async def best(body, n, repeat=REPEAT):
"""Лучшее из `repeat` прогонов, наносекунд на итерацию."""
times = []
for _ in range(repeat):
start = time.perf_counter()
await body(n)
times.append(time.perf_counter() - start)
return min(times) / n * 1e9
ROWS = [
("обычный вызов функции", bench_plain, N),
("await корутины без приостановки", bench_await, N),
("создать корутину и закрыть", bench_create_close, N),
("await завершённого Future", bench_done_future, N // 2),
("await asyncio.sleep(0)", bench_sleep0, N // 4),
]
async def main():
print("PY", sys.version.split()[0], f"| лучшее из {REPEAT} прогонов")
results = {}
for label, body, n in ROWS:
ns = await best(body, n)
results[label] = ns
print(f" {label:<34} {ns:9.1f} нс (по {n:,} итераций)".replace(",", " "))
base = results["обычный вызов функции"]
print(" во сколько раз дороже обычного вызова:")
for label, _, _ in ROWS[1:]:
print(f" {label:<34} x{results[label] / base:7.1f}")
awaited = results["await корутины без приостановки"]
made = results["создать корутину и закрыть"]
print(
f" из цены await ({awaited:.1f} нс) на создание объекта корутины "
f"приходится {made:.1f} нс — {made / awaited * 100:.0f} %"
)
asyncio.run(main())