Программирование PythonПамять и производительностьPython-разработчик серверных приложений

На микробенчмарке первые вызовы Python функции заметно медленнее последующих: какой механизм CPython нужно ...

На микробенчмарке первые вызовы Python-функции заметно медленнее последующих: какой механизм CPython нужно учитывать при интерпретации результата?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

В CPython 3.11 и новее первые вызовы могут быть медленнее из-за адаптивной специализации интерпретатора. Во время выполнения CPython собирает информацию о фактических типах и значениях операций, после чего заменяет некоторые универсальные инструкции специализированными быстрыми вариантами. Поэтому измерение только первых вызовов часто завышает устойчивое время работы.

Исторический контекст

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

В CPython 3.11 появился механизм специализации адаптивного интерпретатора, описанный в PEP 659. Он сохраняет динамическую семантику Python, но пытается ускорить повторяющиеся операции, если их условия выполнения достаточно стабильны.

Постановка проблемы

Если бенчмарк измеряет функцию сразу после запуска процесса, в результат попадают не только полезные вычисления, но и стоимость начальной адаптации байткода. Сравнение такой функции с уже прогретой функцией может привести к ошибочному выводу о производительности.

Проблема особенно заметна в коротких микротестах: время специализации оказывается сопоставимым со временем самой операции. Неверный замер может привести к преждевременной оптимизации кода или к выбору неподходящей реализации.

Подробное решение

CPython начинает с адаптивных инструкций. После нескольких выполнений они могут специализироваться, например под доступ к атрибуту объекта определённого типа или под арифметическую операцию над ожидаемыми типами. Специализированная инструкция использует сохранённые предположения и выполняет меньше общих проверок.

Если предположения перестают выполняться, например в операции начинают участвовать объекты других типов или меняется структура объекта, специализированная инструкция может деоптимизироваться. В этом случае она снова адаптируется либо возвращается к более общему пути выполнения.

Практический вывод: микробенчмарк должен включать прогрев и измерять несколько независимых запусков. Для серьёзных измерений полезны инструменты вроде pyperf, которые помогают отделить прогрев от измеряемой фазы и учитывать разброс результатов.

import timeit def total(items): result = 0 for value in items: result += value return result items = list(range(1000)) print(timeit.timeit("total(items)", globals=globals(), number=1)) print(timeit.timeit("total(items)", globals=globals(), number=10000))

Первый замер включает непрогретое выполнение и потому не сопоставим напрямую со средней стоимостью повторных вызовов. Второй замер обычно лучше отражает поведение горячего участка, хотя и он зависит от версии Python, оборудования и состояния системы.

Специализация не гарантирует ускорение любой функции. Если типы постоянно меняются, код редко выполняется или основное время тратится внутри системного вызова либо расширения на C, выигрыш может быть мал или отсутствовать. Кроме того, поведение механизма зависит от версии CPython и не является универсальным свойством всех реализаций Python.

Ситуация из практики

Команда сравнивала две реализации преобразования числовых данных. Первая измерялась одним вызовом после запуска процесса и выглядела медленнее второй почти вдвое. После добавления прогрева и серии повторных измерений разница исчезла: первоначальный результат отражал адаптацию интерпретатора, а не преимущество алгоритма.

Рассматривались три варианта. Запускать отдельный новый процесс для каждого измерения было просто, но это усиливало влияние холодного старта; измерять один вызов было быстро, но статистически ненадёжно; использовать прогрев и большое число повторов было сложнее, зато позволяло оценить устойчивую производительность.

Выбрали третий вариант и дополнительно проверили несколько версий CPython. В результате команда отказалась от неоправданной оптимизации и получила воспроизводимый бенчмарк, отдельно фиксируя время холодного и прогретого выполнения.

Что кандидаты часто упускают

  1. Означает ли медленный первый вызов, что код требует оптимизации?

Нет. Причиной может быть не алгоритм, а стоимость запуска процесса, импорта модулей, создания объектов и адаптации байткода. Сначала нужно отделить холодный старт от устойчивого режима работы, а затем сравнивать результаты на одинаковой версии Python и одинаковом окружении.

  1. Что произойдёт, если в горячем цикле меняются типы операндов?

Специализация станет менее эффективной: сохранённые предположения будут чаще проверяться и могут нарушаться. Интерпретатор способен деоптимизировать инструкцию, поэтому преимущество прогретого выполнения уменьшится. Это не меняет корректность программы, но может увеличить время работы и сделать результаты бенчмарка более нестабильными.

  1. Можно ли переносить результат такого бенчмарка между реализациями Python?

Нет, напрямую нельзя. Адаптивная специализация относится к внутреннему устройству конкретных версий CPython; PyPy, старые версии CPython и другие реализации используют иные механизмы исполнения. Результат нужно повторно измерять в целевой среде, особенно если решение зависит от микросекундных различий.