Ещё больше интересного о компиляторе Go

В этом материале собраны дополнительные аспекты компилятора Go, которые редко обсуждаются даже среди опытных Go-разработчиков. Базовые этапы и SSA разобраны в первой статье про компилятор Go.

Inline-функции: не всё так просто

Go выполняет инлайнинг функций ещё до SSA-прохода. Однако:

  • Инлайн-лист ведётся в структуре ir.Node, а не в SSA.
  • Компилятор учитывает глубину вложенности, чтобы избежать бесконтрольного роста кода.
  • Даже простые функции могут не быть заинлайнены, если они используют defer, recover или go.
func trivial() int { return 42 } // будет inlined
func withDefer() int {
    defer fmt.Println("hi")
    return 42
} // не будет

Go-оптимизатор: больше про rewrite.go

В каталоге rewrite*.go находятся архитектурно-зависимые правила оптимизации SSA:

  • Например, rewriteAMD64.go содержит:
    (Add x (Neg y)) => (Sub x y)
  • Это даёт математические оптимизации на уровне IR, до генерации ассемблера.

Эти правила — причина, почему x + (-y) становится x - y без вашего ведома.

Zero-cost defer? Не всегда

Go пытается оптимизировать defer, но:

  • Внутри циклов defer остаётся медленным.
  • Если defer можно статически устранить (например, в return-единственной функции), он может быть превращён в inline-вызов.

SSA-представление умеет «упрощать» код

Во время SSA компилятор упрощает выражения:

x := a * 0  // → x = 0
y := a + 0  // → y = a
z := a * 1  // → z = a

Но только если не теряется семантика типов или вызовов (например, a + 0 не упрощается, если a — интерфейс).

Escape-анализ и heap allocation

Escape-анализ выполняется на стадии IR и определяет:

  • Переменные, которые «убегают» из функции, попадают в heap.
  • Влияет на производительность радикально.
func foo() *int {
    x := 5
    return &x // escape: x в heap
}

Инструмент для анализа:

go build -gcflags="-m" file.go

Как Go выбирает инструкции процессора?

Go использует структуру Prog из cmd/internal/obj, которая:

  • Содержит поле As — код инструкции (например, AMOVQ, AADDQ).
  • Работает с абстракцией, которая потом переводится в байты архитектурой (amd64, arm, riscv и т.д.)

Это делает Go компилятор по-настоящему портируемым — он не зависит от внешнего ассемблера (в отличие от C).

Символьная таблица: скрытые зависимости

Во время компиляции typechecking создаёт символьную таблицу:

  • Типы, переменные, функции — всё помещается в types.Sym.
  • Именно тут происходят конфликты имён, даже если они не используются.

Go на других архитектурах

Go поддерживает:

  • amd64, arm, arm64, ppc64, riscv64, wasm, loong64, s390x
  • Каждая архитектура имеет:
    • Свои правила SSA (rewriteARCH.go)
    • Свой план инструкций (obj/ARCH/asm.go)

Всё это собирается одним компилятором, без изменений исходного кода.

Заключение

Компилятор Go — это не просто frontend кода. Он включает полноценную ассемблерную модель, правило-движок переписывания инструкций, SSA-инфраструктуру и архитектурную независимость. Это делает его мощным и быстрым — зачастую быстрее компиляторов C/C++.