Источник: Nuances of Programming
В статье речь идёт о 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 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 находится во вкладке start:
Переменные a и b здесь выделены, как и условие if: позже можно отследить, как меняются эти строки. Код также показывает, каким образом компилятор управляет функцией println, которая разбивается на четыре: printlock, printint, printnl, printunlock. Компилятор автоматически добавляет блокировку и, в зависимости от типа аргумента, вызывает соответствующий метод для корректного вывода.
В нашем примере a и b известны на стадии компиляции, поэтому компилятор может посчитать конечный результат и отметить переменные как ненужные. Проход opt оптимизирует эту часть:
v11 здесь заменён результатом добавления v4 и v5, обозначенных как мёртвый код. Проход opt deadcode удалит его:
Что касается условия if, проход opt отмечает константу true как мёртвый код, он будет удалён:
Затем другой проход упростит поток управления, отметив ненужный блок и условие как бесполезные. Эти блоки будут после удалены другим проходом, работающим с мёртвым кодом:
После всех проходов компилятор Go создаёт промежуточный код на ассемблере Go:
На следующей фазе создаётся машинный код для бинарного файла.
Создание машинного кода
Заключительный этап компиляции — это создание объектного файла (main.o в нашем случае). Его можно дизассемблировать с помощью команды go tool objdump. Ниже схема работы компилятора:
После создания объектного файла можно перейти непосредственно к компоновщику. Используйте команду go tool link — и ваш двоичный код готов!
Читайте также:
Читайте нас в телеграмме и vk
Перевод статьи Vincent Blanchon: Go: Overview of the Compiler