Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (2024)

Я наткнулся на пост вX/Twitter, где Pritam обнаружил, чтоего решение наLeetcode работало медленнее, когда он использовал встроенную функцию min, производительность улучшилась, когда он реализовал min всвоем коде наPython.

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (1)

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

Автор наэтом скриншоте использовал Python 2, который наданный момент уже стал древностью. Запоследние 10лет Python 3получил множество релизов, и последние версиибыли нацелены наулучшение производительности языка. Так действительноли вызовы функций по‑прежнему так сильно влияют напроизводительность вPython?

Мне стало интересно, и я написал три микро‑бенчмарка, чтобы проверить три кейса:

  • Эффект отвызова встроенной функции вцикле

  • Эффект отвызова функции Python вцикле

  • Эффект отвстраивания функции вцикле

Каки ожидалось, результаты показывают, чтопоследние релизы значительно улучшили производительность CPython вовсех трех кейсах.

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

Бенчмарк 1: Проверка скорости выполнения простых инструкций в цикле

Начнем спервого бенчмарка, вкотором мы выполняем простые вычисления внутри цикла, например, вычисляем минимум изсписка значений. Код приведен ниже. Он использует цикл while вместо for, так каквкоде изпоста вTwitter использовался цикл while, и я хотел сохранить его.

def benchmark1(heights): a = 1 b = len(heights) - 1 min_height = heights[0] while a < b: if heights[a] < min_height: min_height = heights[a] a += 1 return min_height

Ниже приведены показатели производительности дляпервого бенчмарка впоследних версиях CPython:

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (2)

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

Появление суперинструкций

Одна изпростых оптимизаций, введенных вCPython,— суперинструкции. Это специальные инструкции байт‑кода, которые создаются путем объединения двух последовательных инструкций определенных типов, которые часто встречаются впарах впрограммах. Давайте разберемся, какэто работает вконтексте этого конкретного бенчмарка.

Наизображении ниже показан байт‑код тела цикла этого бенчмарка дляPython 3.14.0a0(слева) и Python 3.10(справа). Вцикле интерпретатору нужно многократно загружать значения heights[a] и min_height встек перед тем, каксравнить их. Длязагрузки этих значений встек интерпретатор использует инструкцию LOAD_FAST.

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (3)

Можно легко заметить разницу вбайткоде дляразных версий Python. Вверсия 3.10байткод содержит две последовательные инструкции LOAD_FAST, тогда каквверсии 3.14они заменяются одной инструкцией LOAD_FAST_LOAD_FAST.

Это пример суперинструкции. Она создается компилятором вовремя оптимизации после генерации начального байт‑кода программы. Ниже код этой оптимизации вCPython, котораябыла введена врелизе 3.13.

static intinsert_superinstructions(cfg_builder *g){ for (basicblock *b = g->g_entryblock; b != NULL; b = b->b_next) { for (int i = 0; i < b->b_iused; i++) { cfg_instr *inst = &b->b_instr[i]; int nextop = i+1 < b->b_iused ? b->b_instr[i+1].i_opcode : 0; switch(inst->i_opcode) { case LOAD_FAST: if (nextop == LOAD_FAST) { make_super_instruction(inst, &b->b_instr[i + 1], LOAD_FAST_LOAD_FAST); } break; case STORE_FAST: switch (nextop) { case LOAD_FAST: make_super_instruction(inst, &b->b_instr[i + 1], STORE_FAST_LOAD_FAST); break; case STORE_FAST: make_super_instruction(inst, &b->b_instr[i + 1], STORE_FAST_STORE_FAST); break; } break; } } } int res = remove_redundant_nops(g); assert(no_redundant_nops(g)); return res;}

Преимущества суперинструкций

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

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

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

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (4)

Специализация инструкций байт-кода

Вторая оптимизация, значительно улучшающая производительность дляэтого бенчмарка,— это специализация инструкций, введенная врелизе CPython 3.12.

Если вы снова посмотрите набайт‑код изпредыдущего раздела, вы заметите, чтоинтерпретатору нужно многократно выполнять COMPARE_OP и BINARY_OP длявыполнения операций сравнения и инкремента внутри цикла.

Эти инструкции относительно затратны ввыполнении, поскольку они связаны сдинамической диспетчеризацией. Я рассматривал, чтоименно происходит подкапотом встатье «How Many Lines of C it Takes to Execute a + b in Python?». Ниже краткое содержание.

Когда интерпретатору нужно обработать такие инструкции, какBINARY_OP илиCOMPARE_OP, он получает их операнды изстека. Интерпретатор незнает конкретные типы этих объектов‑операндов, будь то целые числа, строки, числа сплавающей запятой иличто‑то еще, и, следовательно, незнает, какобработать данный случай. Интерпретатор определяет, какобработать операцию, выполняя поиск указателя нафункцию внутри объектов‑операндов. Ноэто требует огромного количества операций поиска поуказателям.

  • Сначала интерпретатору нужно разыменовать объект‑операнд.

  • Затем нужно разыменовать указатель наполе PyTypeObject (ob_type), которое содержит таблицы указателей нафункции.

  • Затем интерпретатору нужно разыменовать таблицу указателей нафункции и найти указатель нафункцию.

  • Наконец, нужно разыменовать сам указатель нафункцию, чтобы вызвать функцию.

Следующая иллюстрация показывает этот процесс поиска поуказателям.

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (5)

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

Ноблагодаря специализации медленные инструкции BINARY_OP и COMPARE_OP преобразуются вспециализированные, например BINARY_ADD_INT, где операция сложения выполняется непосредственно винтерпретаторе безвыполнения переходов поуказателям.

Бенчмарк 2: Проверка скорости выполнения с использованием встроенной функции

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

def benchmark2(heights): a = 1 b = len(heights) - 1 min_height = heights[0] while a < b: min_height = min(heights[a], min_height) a += 1 return min_height

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

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (6)

Произошло два изменения, скоторыми связано уменьшение времени выполнения с 17,33секунд вPython 3.10до 6,7секунд вPython 3.14.0a0. Давайте обсудим их.

Ускорение загрузки глобальных объектов

Давайте посмотрим набайт‑код этого бенчмарка.

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (7)

Привыполнении этого кода интерпретатору необходимо загрузить встроенную функцию min() встек. Дляэтого он использует инструкцию LOAD_GLOBAL.

Инструкции LOAD_GLOBAL необходимо найти глобальный именованный объект вдвух словарях. Первый словарь содержит все глобальные переменные втекущей области видимости, авторой— все встроенные функции.

Поиск всловарях выполняетсябыстро, нонемоментально. Благодаря специализации инструкций интерпретатор оптимизирует это винструкцию LOAD_GLOBAL_BUILTIN.

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

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (8)

Оптимизация встроенных функций min и max

Специализация инструкции LOAD_GLOBAL вLOAD_GLOBAL_BUILTIN неявляется основной причиной впечатляющего результата последних версий Python врамках этого бенчмарка. Реальная причина— это специфическая оптимизация, примененная квстроенным функциям min и max.

Винтерпретаторе есть два соглашения длявызова функций: старое— tp_call и новое— vectorcall.

Прииспользовании tp_call создаются промежуточные кортежи и словари дляпередачи аргументов функции, чтоможет приводить кзатратам насоздание других промежуточных объектов (более подробно это описано вPEP 0590). Прииспользовании vectorcall аргументы передаются какчасть вектора, чтоустраняет необходимость всоздании множества промежуточных объектов.

Дорелиза CPython 3.13встроенные функции min и max вызывались сиспользованием tp_call. Это приводило ктому, чточастый вызов функций приводил квыделению и освобождению множества промежуточных объектов. Переход наvectorcall улучшил производительность этих встроенных функций вплоть до 200%, авнашем бенчмарке видно улучшение более чем на 150%.

Дляполучения дополнительного контекста вы можете ознакомиться сPR этого изменения.

Бенчмарк 3: Проверка скорости выполнения с использованием функции Python

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

def pymin(a, b): if a <= b: return a return bdef benchmark3(heights): a = 1 b = len(heights) - 1 min_height = heights[0] while a < b: min_height = pymin(heights[a], min_height) a += 1 return min_height

Сравнение результатов выполнения данного бенчмарка дляпоследних релизов CPython:

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (9)

Согласно данным втаблице, производительность значительно улучшилась с 3.10до 3.12, азатем немного улучшилась для 3.14.0a0.

Этот бенчмарк посути измеряет затраты навыполнение вызова функции Python‑to‑Python (поскольку и вызывающая, и вызываемая функции реализованы наPython).

Доверсии 3.11обработка вызовов функций Python‑to‑Python винтерпретаторебыла сложной и затратной, поскольку это приводило крекурсивному вызову самого интерпретатора дляобработки каждого такого вызова. Эта рекурсиябыла встроена врелизе CPython 3.11, чтопривело кзначительному приросту производительности. Давайте разберем это более детально.

Встраивание вызовов функций Python-to-Python

Интерпретатор начинает выполнение программы Python сфункции main. Впервую очередь подготавливается фрейм стека функции main, после чего идет вызов интерпретатора. Точка входа винтерпретатор— это функция _PyEval_EvalFrameDefault, определенная вceval.c.

Чтотакое фрейм стека? Выполнение каждой функции ввашем коде требует соответствующего фрейма. Он содержит локальные и глобальные переменные этой функции, скомпилированный байт‑код, указатель наинструкцию и другие данные, которые помогают интерпретатору выполнить код. Дляполучения более подробной информации вы можете посмотреть запись моей лекции о внутреннем устройстве виртуальной машины CPython.

Функция _PyEval_EvalFrameDefault содержит огромный оператор switch дляобработки всех инструкций байт‑кода, поддерживаемых интерпретатором. Функция перебирает инструкции полученной функции и выполняет соответствующие операции.

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (10)

Когда вы вызываете другую функцию ввашем коде Python, это приводит кгенерации инструкции байт‑кода CALL. Вот тут все и становится интереснее.

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

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

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

Вы можете ознакомиться собсуждением этого изменения в багтрекере CPython.

Специализация инструкции CALL

Хотя основное улучшение производительности этого бенчмарка связано свстраиванием функций, описанным выше, есть еще одно небольшое улучшение, касающееся выполнения вызовов функций винтерпретаторе— специализация инструкции CALL.

Инструкция CALL является универсальной длявыполнения всех типов вызываемых объектов. Приее обработке интерпретатору необходимо проверить тип вызываемого объекта, например, являетсяли он методом класса, методом экземпляра, функцией иличем‑то еще, и наоснове этого правильно вызвать нужный объект.

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

Заключение

Мы подробно рассмотрели производительность Python, сосредоточив внимание назатратах навызовов функций, вызовах встроенных функций и встраивании кода. Наши бенчмарки показали значительные улучшения производительности сPython 3.10доPython 3.14.0a0. Ниже краткий обзор причин этих улучшений:

  • Суперинструкции: Объединяя последовательные инструкции байт‑кода вединые суперинструкции, такие какLOAD_FAST_LOAD_FAST, CPython уменьшает затраты навыполнение отдельных инструкций байт‑кода. Это улучшение помогает какинтерпретатору, так и процессору работать более эффективно.

  • Специализация инструкций байт‑кода: Новые специализированные инструкции байт‑кода (например, BINARY_ADD_INT) убирают необходимость вмедленной динамической диспетчеризации, ускоряя обычные операции.

  • Оптимизация встроенных функций: Переход отстарого метода tp_call кболеебыстрому vectorcall значительно повысил производительность встроенных функций min и max.

  • Встраивание вызовов функций Python‑to‑Python: Устранив старый способ обработки вызовов функций Python‑to‑Python (который включал громоздкие рекурсивные вызовы интерпретатора), новые версии, начиная сCPython 3.11, сделали эти вызовыбыстрее.

Вцелом, эти изменения демонстрируют постоянные улучшения скорости и эффективности Python. Однако перед тем какиспользовать результаты изэтой статьи, помните, чтосначала необходимо провести профилирование, чтобы найти самые медленные участки кода (закон Амдала).

Вызовы функций в Python по прежнему медленные? Анализ последних оптимизаций в CPython (2024)
Top Articles
Maytag User Manuals - Read online or download PDF
Maytag washer - Free Pdf Manuals Download
Spasa Parish
Rentals for rent in Maastricht
159R Bus Schedule Pdf
Sallisaw Bin Store
Black Adam Showtimes Near Maya Cinemas Delano
Espn Transfer Portal Basketball
Pollen Levels Richmond
11 Best Sites Like The Chive For Funny Pictures and Memes
Things to do in Wichita Falls on weekends 12-15 September
Craigslist Pets Huntsville Alabama
Paulette Goddard | American Actress, Modern Times, Charlie Chaplin
What's the Difference Between Halal and Haram Meat & Food?
R/Skinwalker
Rugged Gentleman Barber Shop Martinsburg Wv
Jennifer Lenzini Leaving Ktiv
Justified - Streams, Episodenguide und News zur Serie
Epay. Medstarhealth.org
Olde Kegg Bar & Grill Portage Menu
Cubilabras
Half Inning In Which The Home Team Bats Crossword
Amazing Lash Bay Colony
Juego Friv Poki
Dirt Devil Ud70181 Parts Diagram
Truist Bank Open Saturday
Water Leaks in Your Car When It Rains? Common Causes & Fixes
What’s Closing at Disney World? A Complete Guide
New from Simply So Good - Cherry Apricot Slab Pie
Drys Pharmacy
Ohio State Football Wiki
FirstLight Power to Acquire Leading Canadian Renewable Operator and Developer Hydromega Services Inc. - FirstLight
Webmail.unt.edu
2024-25 ITH Season Preview: USC Trojans
Metro By T Mobile Sign In
Restored Republic December 1 2022
12 30 Pacific Time
Jami Lafay Gofundme
Stellaris Resolution
Wi Dept Of Regulation & Licensing
Pick N Pull Near Me [Locator Map + Guide + FAQ]
Crystal Westbrooks Nipple
Ice Hockey Dboard
Über 60 Prozent Rabatt auf E-Bikes: Aldi reduziert sämtliche Pedelecs stark im Preis - nur noch für kurze Zeit
Wie blocke ich einen Bot aus Boardman/USA - sellerforum.de
Infinity Pool Showtimes Near Maya Cinemas Bakersfield
Hooda Math—Games, Features, and Benefits — Mashup Math
Dermpathdiagnostics Com Pay Invoice
How To Use Price Chopper Points At Quiktrip
Maria Butina Bikini
Busted Newspaper Zapata Tx
Latest Posts
Article information

Author: Van Hayes

Last Updated:

Views: 5971

Rating: 4.6 / 5 (46 voted)

Reviews: 85% of readers found this page helpful

Author information

Name: Van Hayes

Birthday: 1994-06-07

Address: 2004 Kling Rapid, New Destiny, MT 64658-2367

Phone: +512425013758

Job: National Farming Director

Hobby: Reading, Polo, Genealogy, amateur radio, Scouting, Stand-up comedy, Cryptography

Introduction: My name is Van Hayes, I am a thankful, friendly, smiling, calm, powerful, fine, enthusiastic person who loves writing and wants to share my knowledge and understanding with you.