Deep Engineering

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} раза")