4. Генерация
промежуточного кода
В процессе трансляции исходной программы в целевой код компилятор
может создавать одно или несколько промежуточных представлений различного вида.
Синтаксические деревья являются видом промежуточного представления; обычно они
используются в процессе синтаксического и семантического анализа.
После синтаксического и семантического анализа исходной программы многие
компиляторы генерируют явное низкоуровневое или машинное промежуточное
представление исходной программы, которое можно рассматривать как программу
для абстрактной вычислительной машины. Такое промежуточное представление
должно обладать двумя важными свойствами: оно должно легко генерироваться и
легко транслироваться в целевой машинный язык.
tl = inttofloat(60)
t2 = id3 * tl
t3 = id2 + t2
idl = t3
|
Следует сделать несколько замечаний по поводу трехадресных команд.
Во- первых, каждая трехадресная команда присваивания содержит как минимум
один оператор справа. Таким образом, приведенные команды определяют порядок
выполнения операций — в исходной программе (1.1) умножение выполняется раньше
сложения. Во-вторых, компилятор должен генерировать временные имена для
хранения значений, вычисляемых трехадресными командами. В-третьих, некоторые
"трехадресные команды" наподобие первой и последней в
последовательности (1.3) содержат менее трех операндов.
|
Выход
генератора промежуточного кода на рис. 1.7 состоит из последовательности
трехадресных кодов
В главе 6 мы
рассмотрим основные промежуточные представления, используемые компиляторами. В
главе 5 вы познакомитесь с методами синтаксически управляемой трансляции,
которые будут применены в главе 6 для проверки типов и генерации промежуточного
кода для типичных конструкций языка программирования, таких как выражения, конструкции
управления потоком выполнения и вызовы процедур.
Фаза машинно-независимой оптимизации кода
пытается улучшить промежуточный код, чтобы затем получить более качественный
целевой код. Обычно "более качественный", "лучший"
означает "более быстрый", но могут применяться и другие критерии
сравнения, как, например, "более короткий код" или "код,
использующий меньшее количество ресурсов". Например, непосредственный алгоритм
генерирует промежуточный код (1.3), используя по команде для каждого оператора
в синтаксическом дереве, полученном на выходе семантического анализатора.
Простой алгоритм генерации промежуточного
кода с последующим оптимизатором кода представляет собой рациональный способ
генерации хорошего целевого кода. Оптимизатор может определить, что
преобразование 60 из целого числа в число с плавающей точкой может быть
выполнено единственный раз во время компиляции, так что операция inttofloat может быть устранена
путем замены целого числа 60 числом с плавающей точкой 60.0. Кроме того, t3 используется только один раз — для
передачи значения в idl, так что
оптимизатор может преобразовать (1.3) в более короткую последовательность
tl = id3 * 60.0
(1.4)
idl = id2 + tl
Имеется большой
разброс количества усилий, затрачиваемых различными компиляторами на
оптимизацию на этом этапе. Так называемые "оптимизирующие
компиляторы" затрачивают на эту фазу достаточно много времени, в то время
как другие компиляторы применяют здесь только простые методы оптимизации,
которые существенно повышают скорость работы целевой программы, при этом не
слишком замедляя процесс компиляции. В главах, начиная с 8, детально
рассматриваются методы машинно-зависимой и машинно-независимой оптимизации.
Генератор кода получает в качестве входных данных
промежуточное представление исходной программы и отображает его в целевой
язык. Если целевой язык представляет собой машинный код, для каждой переменной,
используемой программой, выбираются соответствующие регистры или ячейки
памяти. Затем промежуточные команды транслируются в последовательности
машинных команд, выполняющих те же действия. Ключевым моментом генерации кода
является аккуратное распределение регистров для хранения переменных.
Например, при использовании регистров R1 и R2 промежуточный код (1.4) может
транслироваться в машинный код
LDF
|
R2,
|
id3
|
|
MULF
|
R2,
|
R2,
|
#60
|
LDF
|
R1,
|
id2
|
(1.5)
|
ADDF
|
R1,
|
Rl,
|
R2
|
STF
|
idl,
|
Rl
|
Первый операнд каждой команды
определяет приемник. F в каждой команде говорит о том, что команда работает с числами с
плавающей точкой. Код (1.5) загружает содержимое адреса id3 в регистр R2, затем умножает его на константу с
плавающей точкой 60.0. # указывает, что 60.0 следует рассматривать как непосредственное
значение. Третья команда помещает id2 в регистр R1,
а четвертая прибавляет к нему предварительно вычисленное и сохраненное в
регистре R2 значение.
Наконец, значение регистра R1 сохраняется
в адресе idl, так что код
корректно реализует инструкцию присваивания (1.1). Подробнее генерация кода
рассматривается в главе 8.
Это беглое знакомство с генерацией кода полностью
игнорирует важный вопрос о распределении памяти для идентификаторов в исходной
программе. Как вы увидите в главе 7, организация памяти во время выполнения
программы зависит от компилируемого языка. Решения о распределении памяти
принимаются либо в процессе генерации промежуточного кода, либо при генерации
целевого кода.
Комментариев нет:
Отправить комментарий