Компилятор - промежуточная генерация кода

Исходный код может быть непосредственно переведен в его целевой машинный код, тогда зачем вообще нужно переводить исходный код в промежуточный код, который затем транслируется в его целевой код? Давайте посмотрим причины, по которым нам нужен промежуточный код.

Промежуточный код
  • Если компилятор переводит исходный язык на свой целевой машинный язык, не имея возможности генерировать промежуточный код, то для каждой новой машины требуется полный собственный компилятор.

  • Промежуточный код устраняет необходимость в новом полном компиляторе для каждой уникальной машины, сохраняя часть анализа одинаковой для всех компиляторов.

  • Вторая часть компилятора, синтез, изменяется в соответствии с целевой машиной.

  • Становится легче применять модификации исходного кода для улучшения производительности кода, применяя методы оптимизации кода к промежуточному коду.

Промежуточное Представительство

Промежуточные коды могут быть представлены различными способами, и они имеют свои преимущества.

  • High Level IR - высокоуровневое представление промежуточного кода очень близко к самому исходному языку. Они могут быть легко сгенерированы из исходного кода, и мы можем легко применить модификации кода для повышения производительности. Но для оптимизации целевой машины это менее предпочтительно.

  • Низкоуровневое ИК - Это устройство близко к целевой машине, что делает его пригодным для распределения регистров и памяти, выбора набора команд и т. Д. Это хорошо для машинно-зависимых оптимизаций.

Промежуточный код может быть либо специфичным для языка (например, Байт-код для Java), либо независимым от языка (трехадресный код).

Трехадресный код

Генератор промежуточного кода получает входные данные от своего предшественника, семантического анализатора, в форме аннотированного синтаксического дерева. Затем это синтаксическое дерево может быть преобразовано в линейное представление, например постфиксную запись. Промежуточный код, как правило, является машинно-независимым кодом. Следовательно, генератор кода предполагает наличие неограниченного количества памяти (регистра) для генерации кода.

Например:

a = b + c * d;

Генератор промежуточного кода попытается разделить это выражение на подвыражения, а затем сгенерирует соответствующий код.

r1 = c * d;
r2 = b + r1;
a = r2

r используется в качестве регистров в целевой программе.

Трехадресный код имеет не более трех адресных местоположений для вычисления выражения. Трехадресный код может быть представлен в двух формах: четверки и тройки.

четверок

Каждая инструкция в четырехкратном представлении разделена на четыре поля: оператор, arg1, arg2 и результат. Приведенный выше пример представлен ниже в формате четверок:

Op аргумент 1 аргумент 2 результат
* с d r1
+ б r1 r2
+ r2 r1 r3
знак равно r3

троек

Каждая инструкция в представлении триплета имеет три поля: op, arg1 и arg2.Результаты соответствующих подвыражений обозначаются позицией выражения. Тройки представляют сходство с DAG и синтаксическим деревом. Они эквивалентны DAG при представлении выражений.

Op аргумент 1 аргумент 2
* с d
+ б (0)
+ (1) (0)
знак равно (2)

Тройки сталкиваются с проблемой неподвижности кода при оптимизации, поскольку результаты являются позиционными, и изменение порядка или позиции выражения может вызвать проблемы.

Косвенные тройки

Это представление является улучшением по сравнению с представлением троек. Он использует указатели вместо позиции для хранения результатов. Это позволяет оптимизаторам свободно перемещать подвыражение для получения оптимизированного кода.

Объявления

Переменная или процедура должна быть объявлена, прежде чем ее можно будет использовать. Декларация включает в себя выделение места в памяти и запись типа и имени в таблице символов. Программа может быть закодирована и разработана с учетом структуры целевой машины, но не всегда возможно точно преобразовать исходный код в целевой язык.

Принимая всю программу как набор процедур и подпроцедур, становится возможным объявить все имена, локальные для процедуры. Выделение памяти производится последовательно, а имена выделяются памяти в той последовательности, в которой они объявлены в программе. Мы используем переменную смещения и устанавливаем ее в ноль {offset = 0}, которая обозначает базовый адрес.

Исходный язык программирования и архитектура целевого компьютера могут варьироваться в зависимости от способа хранения имен, поэтому используется относительная адресация. В то время как первое имя выделяется из памяти, начиная с ячейки памяти 0 {offset = 0}, следующее имя, объявленное позже, должно быть выделено из памяти рядом с первым.

Пример:

Мы возьмем пример языка программирования Си, где целочисленной переменной назначается 2 байта памяти, а переменной с плавающей запятой назначается 4 байта памяти.

int a;
float b;

Allocation process:
{offset = 0}

   int a;
   id.type = int
   id.width = 2

offset = offset + id.width 
{offset = 2}

   float b;
   id.type = float
   id.width = 4
   
offset = offset + id.width 
{offset = 6}

Чтобы ввести эту деталь в таблицу символов, можно использовать процедуру ввода . Этот метод может иметь следующую структуру:

enter(name, type, offset)

Эта процедура должна создать запись в таблице символов для имени переменной с типом, установленным на тип, и относительным смещением адреса в ее области данных.