В статье речь идёт о Go 1.13

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

Фазы компиляции

Компилирование Go состоит из четырёх фаз, которые можно объединить в два этапа:

  • На первом выполняется анализ исходного кода и по мере синтаксического разбора создаётся абстрактная синтаксическая структура исходного кода, которая называется АСД (абстрактное синтаксическое дерево).
  • На втором этапе вместе с многочисленными оптимизациями происходит трансформация представления исходного кода в машинный код.

Для лучшего понимания используем простую программу:

package main

func main() {
 a := 1
 b := 2
 if true {
 add(a, b)
 }
}

func add(a, b int) {
 println(a + b)
}

Синтаксический разбор

Первая фаза предельно проста. Её описание можно найти в README:

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

Лексический анализатор будет первым пакетом, запущенным для маркирования. Исходный код здесь. Ниже приведён результат:

как маркируется исходный код в Go

После маркирования проводится синтаксический анализ и строится синтаксическое дерево.

Преобразование в АСД

Преобразование в абстрактное синтаксическое дерево можно вывести на экран командой go tool compile -W:

Токенизация

На первой фазе также проводятся оптимизации. Например, встраивание. В нашем примере метод add уже может быть встроенным, так как никаких инструкций CALLFUNC с методом add мы здесь не видим. Теперь снова выполним команду с флагом -l для отключения встраивания:

АСД позволяет компилятору перейти к низкоуровневому промежуточному представлению SSA (Static Single Assignment — статическое одиночное присваивание).

Генерация SSA

Создание формы статистического одиночного присваивания — это фаза оптимизации: устранение мёртвого кода, замена выражений на константы и т.д. Код SSA можно вывести командой GOSSAFUNC=main go tool compile main.go && open ssa.html, с помощью которой создаётся HTML-документ со всеми проходами, сделанными в пакете SSA:

Проходы SSA

SSA находится во вкладке start:

Код SSA

Переменные a и b здесь выделены, как и условие if: позже можно отследить, как меняются эти строки. Код также показывает, каким образом компилятор управляет функцией println, которая разбивается на четыре: printlock, printint, printnl, printunlock. Компилятор автоматически добавляет блокировку и, в зависимости от типа аргумента, вызывает соответствующий метод для корректного вывода.

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

SSA, проход opt

v11 здесь заменён результатом добавления v4 и v5, обозначенных как мёртвый код. Проход opt deadcode удалит его:

SSA, проход opt deadcode

Что касается условия if, проход opt отмечает константу true как мёртвый код, он будет удалён:

Удаление лишнего true

Затем другой проход упростит поток управления, отметив ненужный блок и условие как бесполезные. Эти блоки будут после удалены другим проходом, работающим с мёртвым кодом:

Удаление ненужного потока управления

После всех проходов компилятор Go создаёт промежуточный код на ассемблере Go:

ассемблерный код Go

На следующей фазе создаётся машинный код для бинарного файла.

Создание машинного кода

Заключительный этап компиляции — это создание объектного файла (main.o в нашем случае). Его можно дизассемблировать с помощью команды go tool objdump. Ниже схема работы компилятора:

go tool compile
go tool objdump

После создания объектного файла можно перейти непосредственно к компоновщику. Используйте команду go tool link — и ваш двоичный код готов!

Читайте также:


Перевод статьи Vincent Blanchon: Go: Overview of the Compiler

Предыдущая статьяЭкспоненциальное распределение
Следующая статьяПроблема и решение: присвоение имени файлу