PunchKano begins diphtheria immunisation in five Kano LGsRTP DesportoFrancisco Cabral na final de pares em HangzhouESPN DeportesGrecia derrotó a Alemania por primera vez en la historia y golpeó a Klopp en su debut ante su públicoThe Jerusalem PostIsrair awaits final approval for Tokyo, Miami flights, adds new European destinations for 2027Daily MaverickWe worried AI would make things up, we should also worry when it doesn’tInquirerRains to continue in Visayas, Mindanao due to ITCZ until Sept. 30Bollywood HungamaVinod Kapri's Pyre to release in theaters on October 23, 2026BlickNächstes Amt abgelegt: Jens Spahn zieht sich aus Haushaltsausschuss zurückRapplerPhilippines should fix tax gaps and procurement, not raise tax rates – WBSportstarIndia in Athletics LIVE Updates, Asian Games 2026: Vithya Ramraj breaks National Record to win 400m hurdles bronze; Javelin throw final at 4:45 PM ISTIl Fatto QuotidianoMorto Stefano Milani, il tifoso del Milan diventato famoso su X. Da Bertolucci a Valenti: “Ha lottato come nessuno, mai un passo indietro”7sur7“Pourquoi je perds toujours?”: quand André Agassi taquine Alexander Zverev
The Daily Newsstand · Free, Always
Monday, September 28, 2026

Как ускорить асинхронные генераторы в CPython на 40%, выкинув скрытые исключения

Translate

Недавно я гонял бенчмарки на свежей сборке CPython (ветка с tail-call интерпретатором, PGO + LTO, GCC) и снял флеймграф стандартного бенчмарка async_generators из набора pyperformance (рекурсивный обход дерева на 100 000 нод с глубиной вложенности ~17).

Картина на профиле оказалась фееричной: почти треть всего процессорного времени (29%) сжиралась кодом, который вообще не делал никакой полезной работы.

Рантайм создавал, настраивал, а затем тут же уничтожал объекты исключений StopIteration, о существовании которых Python-код даже не догадывался.

Ниже о том, откуда растут ноги у этой проблемы в Си-коде CPython и как пара правок в genobject.c дали ускорение в 1.33x — 1.53x.

Улики: флеймграф

Вот профиль выполнения бенчмарка (Perforator, 11 200 сэмплов):

Если просуммировать inclusive time по функциям обработки исключений (схлопнув рекурсию):

Функция

Доля циклов CPU

PyGenSetStopIterationValue

12.2%

PyGenFetchStopIterationValue

12.6%

StopIteration_dealloc

4.3%

Итого в трубу

~29.1%

В чем абсурд происходящего в Си?

В CPython асинхронный генератор под капотом использует вспомогательный объект-обёртку PyAsyncGenASend. Когда в Python выполняется async for или await agen._anext__(), вызывается Си-функция PyAsyncGenASendSend().

До патча этот процесс выглядел как перекладывание бумажек через три инстанции:

  1. async_gen_asend_send() дёргает нативный gen_send(), забирает значение из генератора и передает его в async_gen_unwrap_value().

  2. async_gen_unwrap_value() видит, что генератор сделал yield, и вызывает PyGenSetStopIterationValue(value). В этот момент CPython честно аллоцирует инстанс исключения StopIteration, аллоцирует кортеж args, упаковывает туда наше значение и выставляет ошибку в тред-стейт.

  3. Функция уровнем выше (_PyAsyncGenASend_Send()) сразу же вызывает PyGenFetchStopIterationValue(): Она проверяет тип исключения, вытаскивает из него значение обратно, сбрасывает ошибку в треде и… вызывает StopIteration_dealloc, уничтожая только что созданное исключение.

Это исключение никогда не предназначалось для передачи в Python. Оно использовалось тупо как костыльный Си-интерфейс для передачи одного указателя между двумя функциями внутри рантайма.

И если у вас генератор завернут в генератор (например, пайплайн обработки или рекурсия), эта бессмысленная аллокация и очистка происходили на каждом уровне вложенности для каждого элемента.

Что делаем в патче?

Код патча можно разделить на две части:

1. Избавляемся от StopIteration в горячем пути

Мы разделяем распаковку значения на два пути:

  • Добавляем Си-хелпер async_gen_unwrap_send(), который возвращает статус через нативный enum PySendResult (PYGEN_RETURN, PYGEN_NEXT, PYGEN_ERROR), а само значение отдает через указатель PyObject **presult. Никаких исключений в треде.

  • Переписываем PyAsyncGenASendSend на прямую работу с этим хелпером. Забираем strong reference через Py_NewRef, прибиваем внутреннюю обертку _PyAsyncGenWrappedValue и отдаем результат.

  • Для совместимости питонячий метод asend.send() (async_gen_asend_send) оставляем тонкой оберткой, которая при необходимости всё ещё поднимает StopIteration, чтобы не ломать публичное API.

2. Микрооптимизация деаллокатора

Заодно в async_gen_asend_dealloc нашлась еще одна мелкая свинья:

// До:
if (PyObject_CallFinalizerFromDealloc(self)) return;

// После:
if (ags->ags_state == AWAITABLE_STATE_INIT
    && PyObject_CallFinalizerFromDealloc(self))
{
    return;
}

Финализатор у asend объекта нужен ровно для одного: бросить RuntimeWarning, если корутину создали, но ни разу не заэвейтили. Если объект уже крутится в итерации (AWAITABLE_STATE_ITER) или закрыт (CLOSED), звать тяжелый PyObject_CallFinalizerFromDealloc нет никакого смысла — варнингов там быть не может.

Результаты

Бенчмарк async_generators из pyperformance. Тот же коммит, то же железо, единственная разница — патч в genobject.c:

Конфигурация

До патча

После

Ускорение

GCC tail-call, PGO+LTO, полный pyperformance

585 ms

440 ms

1.33x (+33%)

GCC tail-call, PGO+LTO, изоляция на 4 ядрах (taskset)

625 ms

444 ms

1.41x (+41%)

GCC tail-call, без PGO/LTO

552 ms

384 ms

1.44x (+44%)

2-vCPU VM, GCC tail-call, --fast

807 ms

527 ms

1.53x (+53%)

Остальные тесты из pyperformance остались в рамках шума измерений (что ожидаемо, так как патч изолирован в кишках PyAsyncGen). Полный сьют тестов Python (test -j8, 491 тест) проходит без ошибок.

PR заслан в апстрим CPython: gh-158315 (ссылка на PR).

Мораль

Если ваш рантайм на C/C++ использует механизм исключений для регулярной передачи данных в горячем цикле — рано или поздно в профилировщике вы увидите не свою логику, а работу аллокатора. Даже пара простых правок по замене псевдо-исключений на возврат enum-статуса может выкинуть 30% оверхеда на ровном месте.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.