Разбираемся с компилятором Go



Книга Разбираемся с компилятором Go

В статье речь идёт о 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 — и ваш двоичный код готов!


503   0  

Comments

    Ничего не найдено.