среда, 4 августа 2010 г.

Генерация и оптимизация кода


4. Генерация промежуточного кода
В процессе трансляции исходной программы в целевой код компилятор может создавать одно или несколько промежуточных представлений различного вида. Синтаксические деревья являются видом промежуточного представления; обычно они используются в процессе синтаксического и семантического анализа.
После синтаксического и семантического анализа исходной программы мно­гие компиляторы генерируют явное низкоуровневое или машинное промежуточ­ное представление исходной программы, которое можно рассматривать как про­грамму для абстрактной вычислительной машины. Такое промежуточное пред­ставление должно обладать двумя важными свойствами: оно должно легко гене­рироваться и легко транслироваться в целевой машинный язык.

tl = inttofloat(60)
 t2 = id3 * tl                      
t3 = id2 + t2
idl = t3
(1.3)
Следует сделать несколько замечаний по поводу трехадресных команд. Во- первых, каждая трехадресная команда присваивания содержит как минимум один оператор справа. Таким образом, приведенные команды определяют порядок вы­полнения операций — в исходной программе (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, организация памяти во время выполнения программы за­висит от компилируемого языка. Решения о распределении памяти принимаются либо в процессе генерации промежуточного кода, либо при генерации целевого кода.

Комментариев нет:

Отправить комментарий