Оптимизации компилятора

Компилятор может улучшить код, не меняя его результата, — этим занимаются оптимизации.

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

Оптимизатор работает между генерацией IR и финальным кодом. Его золотое правило: результат программы обязан остаться прежним. Менять можно форму, но не смысл. Рассмотрим две классические, наглядные оптимизации.

Свёртка констант

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

Зачем процессору в рантайме считать 2 + 3 * 4, если ответ 14 известен уже при компиляции? Оптимизатор находит поддеревья из одних чисел и заменяет их результатом.

class Num:
    def __init__(self, v): self.value = v
    def __repr__(self): return str(self.value)
class BinOp:
    def __init__(self, op, l, r): self.op, self.left, self.right = op, l, r
    def __repr__(self): return "(" + repr(self.left) + self.op + repr(self.right) + ")"

def fold(node):
    if isinstance(node, Num):
        return node
    left = fold(node.left)
    right = fold(node.right)
    if isinstance(left, Num) and isinstance(right, Num):
        ops = {"+": left.value + right.value, "*": left.value * right.value}
        return Num(ops[node.op])
    return BinOp(node.op, left, right)

tree = BinOp("+", Num(2), BinOp("*", Num(3), Num(4)))
print("до:  ", tree)
print("после:", fold(tree))

Вывод:

до:   (2+(3*4))
после: 14

Всё дерево свернулось в одно число. В рантайме не останется ни одной арифметической операции.

Удаление мёртвого кода

Удаление мёртвого кода (dead code elimination) — выбрасывание инструкций, результат которых нигде не используется или которые недостижимы.

Если переменную вычислили, но ни разу не прочитали, её вычисление бессмысленно. То же с кодом после return — он недостижим. Покажем простую идею на «инструкциях присваивания».

program = [
    ("assign", "a", "1"),
    ("assign", "b", "2"),   # b нигде не используется (мёртвый код)
    ("use", "a"),
]

used = {name for kind, name, *_ in program if kind == "use"}
clean = [ins for ins in program
         if ins[0] != "assign" or ins[1] in used]

for ins in clean:
    print(ins)

Вывод:

('assign', 'a', '1')
('use', 'a')

Как работает под капотом

Обе оптимизации — снова обходы. Свёртка обходит AST снизу вверх и заменяет числовые поддеревья. Удаление мёртвого кода анализирует, какие значения реально используются, и выбрасывает остальное. Реальные оптимизаторы делают десятки таких проходов: инлайнинг функций, вынос инвариантов из циклов, распределение регистров. Но принцип один — переписать программу, сохранив её наблюдаемое поведение.

Частые ошибки

Опаснейшая ошибка оптимизатора — изменить результат. Например, «свернуть» 10 / 0 в момент компиляции и убрать ошибку, которая должна была случиться в рантайме. Или удалить присваивание, у которого есть побочный эффект (например, вызов функции). Оптимизатор обязан учитывать побочные эффекты, иначе он сломает программу.

Вторая — оптимизировать слишком агрессивно ценой времени компиляции. Иногда простой код компилируется быстрее, чем оптимизатор успевает его «улучшить». Поэтому уровни оптимизации (-O0, -O2) дают выбор баланса.

Итог

  • Оптимизация меняет форму кода, строго сохраняя его результат.
  • Свёртка констант вычисляет константные выражения при компиляции.
  • Удаление мёртвого кода выбрасывает неиспользуемые и недостижимые инструкции.
  • Оптимизатор обязан учитывать побочные эффекты и не менять наблюдаемое поведение.
Проверьте себя
1. Что делает свёртка констант?
AУдаляет все переменные
BВычисляет константные выражения на этапе компиляции
CЗамедляет программу
DМеняет результат программы
2. Какое золотое правило любой оптимизации?
AПрограмма должна стать короче любой ценой
BНаблюдаемый результат программы обязан остаться прежним
CНужно удалить все циклы
DОптимизация должна менять логику
3. Почему оптимизатор должен учитывать побочные эффекты?
AОни не важны
BИначе он может удалить присваивание с вызовом функции или скрыть ошибку рантайма
CЧтобы программа стала медленнее
DПобочных эффектов не бывает