Оптимизация производительности кода всегда была актуальной задачей для разработчиков. Особенно это касается языков программирования с динамической типизацией и интерпретируемых языков, таких как Python. Множество годами проверенных хитростей помогали сделать код быстрым и эффективным. Однако эволюция самого интерпретатора CPython внесла свои коррективы, из-за которых ряд классических приемов стал неактуален или даже излишен. Одним из таких приемов является создание локальных алиасов для часто вызываемых функций, например, для встроенной функции len().
Долгое время практика создания локальной переменной, ссылающейся на глобальную функцию, была оправдана и достаточно широко применялась в целях оптимизации кода. Если рассмотреть ситуацию с вызовом len() в горячем цикле с миллионом итераций, то напрямую брать встроенную функцию из глобального пространства имен означало каждый раз выполнять достаточно дорогую операцию по поиску функции. В Python поиск имени в пространстве глобальных переменных и в пространстве встроенных функций — это не просто быстрый прямой доступ, а многоступенчатая операция, включающая хеширование имени и несколько обращений к словарям. Перекладывание такой функции в локальную переменную избавляло от затрат на поиск и заменяло тяжелый глобальный поиск быстрым обращением к локальной переменной. Это объяснялось особенностями виртуальной машины CPython, с ее инструкциями LOAD_GLOBAL и LOAD_FAST.
Первая делает поиск по хеш-таблицам, вторая же — просто берет значение из локального массива переменных. Тем не менее в новой версии интерпретатора CPython 3.11 появилась кардинальная модернизация, названная специализацией инструкций. Это означает, что инструкция загрузки глобального имени адаптируется под конкретное используемое имя уже во время выполнения. После первой итерации интерпретатор запоминает тип операции и оптимизирует последующие вызовы.
В частности, для встроенных функций была реализована специализированная инструкция LOAD_GLOBAL_BUILTIN, которая позволяет пропускать обращение к глобальному словарю и сразу обращаться к встроенным функциям, используя кэшированный индекс. Таким образом, тяжелая операция глобального поиска сводится к быстрому доступу почти на том же уровне, что и локальное чтение. Результат стал очевиден — прирост скорости от создания локального алиаса глобальной функции практически исчез. Бенчмарки показывают, что производительность вызова len() напрямую из глобального пространства теперь сравнима с вызовом через локальный алиас. Это полностью изменяет подход к оптимизации кода: что раньше было критичным улучшением, теперь стало неактуальным, а в некоторых случаях — даже вредным с точки зрения читаемости кода.
Но стоит упомянуть, что это не значит полной отмены оптимизаций по доступу к функциям и переменным. В сценариях, когда вызываются функции из импортированных модулей через полное имя с точечной нотацией, например, math.sin, ситуация остается сложнее. Здесь используется не одна, а сразу несколько инструкций: загрузка модуля через LOAD_GLOBAL и последующий поиск атрибута через LOAD_ATTR. Несмотря на улучшения, этот многоступенчатый процесс все еще отнимает больше времени, чем наличие локального алиаса.
Следовательно, в такой ситуации создание локальной переменной для ссылки на функцию math.sin может обеспечить заметный прирост производительности за счет сокращения числа инструкций и уровней обращений к словарям. Также стоит отметить, что импорт функции напрямую, используя конструкцию from math import sin, позволяет избавиться от необходимости выполнять LOAD_ATTR и ускоряет вызов. Правильный способ организации импортов и локальных ссылок становится полезным инструментом в высоконагруженных и чувствительных к производительности частях кода. В целом, изменения в CPython 3.
11 представляют собой важное улучшение производительности без необходимости от программиста добавлять лишний костыльный код. Это позволяет писать более чистый, понятный и поддерживаемый код без избыточной оптимизации, жертвуя удобочитаемостью ради малозаметного прироста скорости. Современные версии Python автоматизируют множество оптимизаций на уровне интерпретатора, оставляя разработчикам меньше поводов заниматься низкоуровневыми трюками, которыми занимались раньше. Тем не менее, ключевым остается понимание устройства Python — как именно оно работает, как интерпретатор ищет переменные и функции, зачем нужны локальные и глобальные пространства имен и как связаны байткод и инструкции виртуальной машины. Это значительно помогает принимать осознанные решения об оптимизации, а также строить эффективный код, не жертвуя его удобочитаемостью и сопровождаемостью.
Финальная мысль такова: оптимизации — это живой организм, который подстраивается под развитие экосистемы. Что было необходимостью десять лет назад, сегодня может остаться в прошлом. Хороший разработчик не гонится слепо за всеми трюками, а знает, когда и зачем их применять, а когда лучше полагаться на возможности современного интерпретатора и писать чистый, читаемый код. Именно такой подход обеспечит лучший результат и в производительности, и в удобстве поддержки программных продуктов на Python.