Компилятор Go (gc) — это многоступенчатая система, превращающая исходный код Go в машинный код. Ниже — нестандартный взгляд на внутреннюю кухню компиляции, включая работу ассемблерной машины, SSA и нетривиальные детали.
Этапы компиляции в Go
- Парсинг — преобразование текста в синтаксическое дерево (AST).
- Проверка типов — через пакет
go/types. - Преобразование в промежуточное представление (IR).
- Оптимизация IR через SSA (Static Single Assignment).
- Генерация ассемблера.
- Сборка объектного файла.
Что мало кто знает
1. SSA как единая форма представления
Go использует SSA (Static Single Assignment) с 2016 года. Это внутреннее представление, в котором каждая переменная присваивается только один раз. Благодаря этому оптимизации становятся тривиальными.
a := 1
b := a + 2
c := b * 3
В SSA:
v1 = 1
v2 = v1 + 2
v3 = v2 * 3
Компилятор cmd/compile/internal/ssa создает граф потока управления (CFG), где каждая инструкция — это нода.
SSA IR Go не совместим с LLVM — у Go своя модель, заточенная под скорость компиляции, а не многоуровневую оптимизацию.
2. Ассемблерная «машина»: cmd/internal/obj
Go использует собственную виртуальную ассемблерную машину, представленную в виде пакета cmd/internal/obj.
Эта машина:
- Работает как внутренний DSL для генерации инструкций.
- Имеет абстрактные инструкции, которые позже мапятся на реальные (
MOVQ,ADDQ, и т.д.). - Поддерживает rewriting rules — шаблоны преобразования инструкций.
Пример правила (в Go ASM):
(MOVQconst [c] _) => (LoadConstant c)
Это значит: если инструкция — MOVQconst с константой, заменить её на более эффективную LoadConstant. Это позволяет делать паттерн-матчинг на уровне инструкций без необходимости писать вручную под каждую архитектуру.
3. Лейаут блоков и физические регистры
Go компилятор реально моделирует регистры, включая spill/reload инструкции, и делает live-range анализ.
Физические регистры назначаются поздно, и аллокатор регистров использует Linear Scan Register Allocation, что намного быстрее, чем Graph Coloring (используемый в LLVM).
4. Инструкции, которых вы не видите
Если вы скомпилируете Go-файл с флагами:
go build -gcflags="-S" main.go
Вы увидите Go-ассемблер, но не увидите промежуточные инструкции SSA. Чтобы их увидеть:
GO_SSADUMP=all ./main
Это даст IR на стадии SSA — с блоками, инструкциями и виртуальными регистрами.
Пример: SSA и ASM
func sum(a, b int) int {
return a + b
}
SSA-представление:
b1:
v1 = Arg <int>
v2 = Arg <int>
v3 = Add v1 v2
Ret v3
Ассемблерная форма (для amd64):
MOVQ a+0(FP), AX
MOVQ b+8(FP), CX
ADDQ CX, AX
MOVQ AX, ret+16(FP)
RET
Интересные файлы в исходниках Go
src/cmd/compile/internal/ssa/rewriteAMD64.go— правила трансляции SSA в ассемблер.src/cmd/internal/obj/— абстрактная ассемблерная машина.src/cmd/compile/internal/base— управление фазами компиляции.
Дополнительное
- Функции с именами
deferкомпилируются в два прохода: один для тела, другой для обработки «defer stack». - Inline-функции разворачиваются на SSA-этапе.
- Nil-указатели в Go часто реализуются через явные
CMPиJEQ, а не через инструкции MMU.
Полезные команды
Показать SSA граф:
GOSSAFUNC=sum go build -gcflags="-S" main.go
Создаст .html граф SSA.
Показать полный ASM:
go tool compile -S main.go
Заключение
Компилятор Go — это не просто парсер и генератор кода. Это высокоорганизованная система с собственной IR, виртуальной ассемблерной машиной и оптимизациями, которые делают его компиляцию молниеносной.
Go не использует LLVM, и в этом его сила — полный контроль над кодогенерацией.