Deep Engineering

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())