Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Интерпретатор Python: цикл выполнения

Интерпретатор Python: байткод и цикл выполнения

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

Прямой ответ

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

import dis

def total(items):
    s = 0
    for x in items:
        s += x
    return s

dis.dis(total)
Этап Что происходит
Компиляция Исходный код преобразуется в байткод
Кадр вызова Создаётся контекст выполнения функции
Цикл интерпретатора Инструкции байткода выполняются последовательно

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

Кадры вызовов и глобальная блокировка

Два аспекта выполнения определяют поведение при отладке и конкурентности.

Кадр вызова. Каждый вызов функции создаёт кадр с локальными переменными и позицией выполнения, поэтому стек кадров отражает цепочку вызовов. Глубокая рекурсия увеличивает стек, поэтому достигается предел.

Глобальная блокировка. Цикл интерпретатора удерживает блокировку, поэтому один поток выполняет байткод в момент времени. О влиянии — в статье про GIL.

Практическое следствие: трассировка стека отражает цепочку кадров, поэтому место ошибки определяется по стеку, а не по исходному коду.

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

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

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

Вызов функции внутри цикла. Каждый вызов создаёт кадр, поэтому накладные расходы умножаются на число итераций.

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

Об очистке объектов и подсчёте ссылок — в статье про garbage collector.

Оптимизация инструкций и кэш адаптивности

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

Специализация инструкции. Инструкция, выполняемая с объектами одного типа, заменяется специализированным вариантом, поэтому обработка выполняется быстрее. Изменение типа объекта возвращает выполнение к общему варианту.

Влияние на горячие участки. Участок, выполняемый многократно, специализируется, поэтому время на одну итерацию сокращается. Однократный участок не получает выгоды от специализации.

Инвалидация специализации. Изменение типа обрабатываемого объекта инвалидирует специализированный вариант, поэтому выполнение возвращается к общему.

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

Практика: анализ выполнения кода

1. Дизассемблируйте участок кода. Перечень инструкций показывает число операций, поэтому избыточные вычисления видны без выполнения.

2. Проверяйте глубину стека при рекурсии. Каждый вызов создаёт кадр, поэтому глубокая рекурсия достигает предела стека.

3. Выносите неизменяемые вычисления из цикла. Инструкция, выполняемая на каждой итерации, умножается на число итераций.

4. Сокращайте число вызовов функций в цикле. Каждый вызов создаёт кадр, поэтому накладные расходы накапливаются.

5. Используйте встроенные функции для типовых операций. Операция выполняется на уровне интерпретатора, поэтому число инструкций меньше.

6. Проверяйте трассировку стека при ошибке. Стек отражает цепочку кадров, поэтому место ошибки определяется по стеку.

7. Учитывайте кэш байткода. Повторный импорт не выполняет компиляцию, поэтому изменение файла требует инвалидации кэша.

Рекомендации:

  1. Различайте время компиляции и время выполнения. Компиляция выполняется один раз при импорте, выполнение — при каждом вызове.
  2. Учитывайте стоимость создания кадра. Вызов функции требует создания кадра, поэтому избыточные вызовы замедляют выполнение.
  3. Проверяйте влияние на блокировку. Цикл интерпретатора удерживает блокировку, поэтому вычислительные задачи не выполняются параллельно в потоках.
  4. Фиксируйте предел рекурсии. Глубокая рекурсия достигает предела стека, поэтому замена на итеративный вариант исключает ошибку.
  5. Пересматривайте горячие участки кода. Участок, выполняемый на каждой итерации, влияет на общее время сильнее однократного участка.

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

Контекст: функция с очевидной логикой выполнялась заметно дольше ожидаемого.

  1. Диагностика. Дизассемблирование показало, что внутри цикла выполнялся вызов функции и повторное вычисление значения, не зависящего от итерации. Число инструкций на итерацию было высоким, поэтому общее время росло пропорционально.
  2. Причина. Вычисление, результат которого не изменялся, выполнялось на каждой итерации. Вызов функции внутри цикла создавал кадр, поэтому накладные расходы умножались на число итераций.
  3. Действия. (а) Вынесли неизменяемое вычисление за пределы цикла; (б) заменили вызов функции внутри цикла на встроенную операцию, выполняемую на уровне интерпретатора; (в) дизассемблировали участок до и после изменения и сравнили число инструкций; (г) измерили время выполнения на фактическом объёме данных; (д) проверили другие участки с циклами на наличие той же структуры.
  4. Результат. Время выполнения сократилось, поскольку число инструкций на итерацию уменьшилось.

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

FAQ

Что показывает дизассемблирование кода? Перечень инструкций байткода, поэтому число операций в участке кода оценивается без выполнения. Избыточные вычисления видны в перечне.

Что такое кадр вызова? Контекст выполнения функции с локальными переменными и позицией. Каждый вызов создаёт кадр, поэтому стек отражает цепочку вызовов.

Почему глубокая рекурсия завершается ошибкой? Каждый вызов создаёт кадр, поэтому стек растёт. При достижении предела глубины рекурсии выполнение завершается ошибкой.

Сохраняется ли скомпилированный байткод? Да, поэтому повторный импорт модуля не выполняет компиляцию. Изменение исходного файла инвалидирует кэш.

Источники: Python dis, datamodel, inspect — ссылки в шапке.