Как механизм vectorcall сокращает накладные расходы вызова объектов в CPython?
vectorcall — внутренний протокол CPython, который позволяет передавать аргументы вызываемому объекту без обязательного создания временных кортежа позиционных аргументов и словаря именованных аргументов. Это уменьшает число аллокаций, копирований и промежуточных преобразований, поэтому особенно заметно ускоряет частые вызовы небольших функций и методов.
Обычный универсальный механизм вызова должен был представить аргументы в удобной, но дорогой форме: позиционные аргументы — в кортеже, именованные — в словаре. Для вызовов с небольшим числом аргументов стоимость подготовки этих структур могла быть сопоставима со стоимостью тела самой функции.
В CPython появился специализированный протокол vectorcall, формализованный в рамках PEP 590. Его цель — сохранить единый интерфейс вызова, но передавать аргументы в более компактном внутреннем представлении и дать вызываемым объектам возможность обрабатывать их без лишних промежуточных объектов.
В горячем цикле функция может выполняться очень быстро, но вызываться миллионы раз. Если каждый вызов сопровождается созданием временных контейнеров для аргументов, растут время выполнения, количество аллокаций и нагрузка на управление памятью.
Неверно считать, что vectorcall автоматически ускоряет любой вызов Python. Выигрыш зависит от типа вызываемого объекта, формы вызова, наличия *args и **kwargs, дескрипторов, обёрток и версии CPython. Кроме того, это оптимизация реализации CPython, а не гарантия языка Python.
При поддержке vectorcall аргументы обычно передаются как массив ссылок на объекты. Информация об именованных аргументах передаётся отдельно, поэтому для простого вызова не требуется заранее собирать полноценный кортеж и словарь.
Вызываемый объект получает указатель на внутреннюю функцию обработки вызова и набор аргументов. Он может сразу проверить их количество, сопоставить позиции с параметрами и выполнить тело функции. Для встроенных функций это особенно эффективно: граница между интерпретатором и C-реализацией проходится с меньшим количеством промежуточных действий.
Для обычной Python-функции CPython также использует быстрый путь вызова и размещает значения параметров непосредственно во внутренних структурах кадра, когда это возможно. Это не означает, что кортежи и словари никогда не создаются: конструкции вроде явного *args, **kwargs, сохранения аргументов или передачи их дальше могут потребовать материализации этих объектов.
Практический вывод — не пытаться вручную «включить» vectorcall в прикладном Python-коде. Оптимизируют саму структуру горячего участка: уменьшают число вызовов, избегают лишних обёрток и проверяют результат профилированием. При разработке расширения на C можно реализовать протокол vectorcall, но это требует соблюдения правил управления ссылками и точного соответствия ABI/API используемой версии Python.
В обработчике миллионов небольших записей функция нормализации вызывалась для каждого поля. Профилирование показало, что тело нормализатора занимало мало времени, а суммарные накладные расходы вызовов были значительными.
Рассматривались три варианта: оставить вызовы без изменений, объединить обработку нескольких записей в один вызов или перенести простой нормализатор в расширение с поддержкой vectorcall. Первый вариант был самым безопасным, но не уменьшал стоимость вызовов; второй сокращал накладные расходы, но усложнял интерфейс и увеличивал размер пакета данных; третий давал быстрый путь, но повышал стоимость сопровождения и зависимость от CPython.
Выбрали пакетную обработку, а vectorcall оставили для узкого C-расширения, где профилирование подтвердило устойчивый выигрыш. Решение было принято не по названию оптимизации, а после измерения времени, количества аллокаций и влияния на задержку. Важный результат — сокращение количества вызовов часто даёт более предсказуемый эффект, чем попытка оптимизировать сам протокол вызова.
1. Устраняет ли vectorcall создание всех объектов, связанных с аргументами?
Нет. Он сокращает типичные промежуточные представления, но не отменяет семантическую необходимость создать объекты, которые явно передаются как значения. Если вызываемый код получает *args или **kwargs, ему могут понадобиться кортеж и словарь для представления этих аргументов. Также такие структуры могут появиться при сохранении или повторной передаче аргументов.
2. Одинаково ли vectorcall ускоряет Python-функции, встроенные функции и пользовательские объекты?
Нет. Поддержка и выигрыш зависят от конкретного типа объекта и пути вызова. Встроенные объекты на C часто получают заметную пользу, поскольку могут напрямую обработать массив аргументов. Python-функции тоже используют быстрые внутренние пути, но итоговое время включает интерпретацию байткода, проверку параметров и выполнение тела. Пользовательский объект ускорится только при корректной реализации соответствующего протокола.
3. Можно ли считать vectorcall переносимой особенностью Python-кода?
Нет. Это деталь реализации CPython, поэтому код приложения не должен зависеть от её наличия или конкретного выигрыша. Другая реализация Python, например PyPy, может применять иные оптимизации. Переносимую оптимизацию следует обосновывать алгоритмом и структурой данных, а использование vectorcall — рассматривать как специализированный приём для CPython-расширений после измерений.