Лекции по конструированию компиляторов - Часть 6
Автор: В.А.Серебряков
Глава 4. Промежуточные представления программы
В процессе трансляции программы часто используют промежуточное
представление (ПП) программы, предназначенное прежде всего для
удобства генерации кода и/или проведения различных оптимизаций
программы. Сама форма ПП зависит от целей его использования.
Наиболее часто используемыми формами ПП является
ориентированный граф (или, в частности, абстрактное
синтаксическое дерево), тройки, четверки, префиксная или
постфиксная запись, атрибутированное абстрактное дерево.
4.1. Представление в виде ориентированного графа
Простейшей формой промежуточного представления является
синтаксическое дерево программы. Более полную информацию о
входной программе дает ориентированный ациклический граф
(ОАГ), в котором в одну вершину объединены вершины
синтаксического дерева, представляющие общие подвыражения.
Синтаксическое дерево и ОАГ для оператора присваивания a:=b*-
c+b*-c приведены на рис. 4.1.
На рис. 4.2. приведены два представления в памяти
синтаксического дерева на рис. 4.1.а). Каждая вершина
кодируется записью с полем для операции и полями для
указателей на потомков. На рис. 4.2.б) вершины размещены в
массиве записей и индексом (или входом) вершины служит
указатель на нее.
4.2. Трехадресный код
Трехадресный код- это последовательность операторов вида x:= y
op z, где x,y и z - имена, константы или сгенерированные
компилятором временные объекты. Здесь op - двуместная
операция, например операция плавающей или фиксированной
арифметики, логическая или побитовая. В правую часть может
входить только один знак операции.
:= :=
/\ /\
/ \ / \
a + a +
/ \ / \
/ \ | |
* * \ /
/ \ / \ *
/ \ / \ / \
b - b - / \
| | b -
| | |
c c |
c
а) б)
Рис. 4.1
66
+------------+ +------------+
10 | := | | | | | 0 | id | b | |
+------+---+-+ |----+---+---|
v | 1 | id | c | |
+--------+ | |----+---+---|
9 | id | a | | 2 | - | 1 | |
+--------+ v |----+---+---|
+------------+ 3 | * | 0 | 2 |
7 | + | | | | | |----+---+---|
+------+---+-+ 4 | id | b | |
+-+ +------------+ |----+---+---|
| | 5 | id | c | |
v v |----+---+---|
+------------+ +------------+ 6 | - | 5 | |
3 | * | | | | 8 | * | | | | | |----+---+---|
+----------+-+ +------+---+-+ 7 | + | 3 | 8 |
v | v | |----+---+---|
+--------+ | +--------+ | 8 | * | 4 | 6 |
0 | id | b | | 4 | id | b | | |----+---+---|
+--------+ v +--------+ v 9 | id | a | |
+------------+ +------------+ |----+---+---|
2 | - | | | | 6 | - | | | | 10| := | 9 | 7 |
+------+-----+ +------+-----+ +------------+
v v
+--------+ +--------+
1 | id | c | 5 | id | c | б)
+--------+ +--------+
а)
Рис. 4.2
Составные выражения должны быть разбиты на подвыражения, при
этом могут появиться временные имена (переменные). Смысл
термина "трехадресный код" в том, что каждый оператор обычно
имеет три адреса: два для операндов и один для результата.
Трехадресный код - это линеаризованное представление
синтаксического дерева или ОАГ, в котором явные имена
соответствуют внутренним вершинам дерева или графа. Например,
выражение x+y*z может быть протранслировано в
последовательность операторов
t1:=y*z
t2:=x+t1
где t1 и t2 - имена, сгенерированные компилятором.
В виде трехадресного кода представляются не только
двуместные операции, входящие в выражения. В таком же виде
представляются операторы управления программы и одноместные
операции. В этом случае некоторые из компонент трехадресного
кода могут не использоваться. Например, условный оператор
if A>B then S1 else S2
может быть представлен следующим кодом:
t:=A-B
JGT t,S2
.....
Здесь JGT - двуместная операция условного перехода, не
вырабатывающая результата.
Разбиение арифметических выражений и операторов управления
делает трехадресный код удобным при генерации машинного кода и
оптимизации. Использование имен промежуточных значений,
вычисляемых в программе, позволяет легко переупорядочивать
трехадресный код.
t1 := -c t1 := -c
t2 := b * t1 t2 := b * t1
t3 := -c t5 := t2 + t2
t4 := b * t3 a := t5
t5 := t2 + t1
a := t5
а) б)
Рис. 4.3
Представления синтаксического дерева и графа рис. 4.1 в виде
трехадресного кода дано на рис. 4.3.а) и 4.3.б),
соответственно.
Трехадресный код - это абстрактная форма промежуточного
кода. В реализации трехадресный код может быть представлен
записями с полями для операции и операндов. Рассмотрим три
реализации трехадресного кода: четверки, тройки и косвенные
тройки.
Четверка - это запись с четырьмя полями, которые будем
называть op, arg1, arg2 и result. Поле op содержит код
операции. В операторах с унарными операциями типа x:=-y или
x:=y arg2 не используется. В некоторых операциях (типа
"передать параметр") могут не использоваться ни arg2, ни
result. Условные и безусловные переходы помещают в result
метку перехода. На рис. 4.4 приведены четверки для a:=b*-c+b*-
c. Они получены из трехадресного кода рис. 4.3.а.
Обычно содержимое полей arg1, arg2 и result - это указатели
на входы таблицы символов для имен, представляемых этими
полями. Временные имена вносятся в таблицу символов по мере их
генерации.
Чтобы избежать внесения новых имен в таблицу символов, на
временное значение можно ссылаться, используя позицию
вычисляющего его оператора. В этом случае трехадресные
операторы могут быть представлены записями только с тремя
полями: op, arg1 и arg2, как это показано на рис. 4.4.б. Поля
arg1 и arg2 - это либо указатели на таблицу символов (для
имен, определенных программистом, или констант), либо
указатели на тройки (для временных значений). Такой способ
представления трехадресного кода называют тройками. Тройки
соответствуют представлению синтаксического дерева или ОАГ с
помощью массива вершин.
Числа в скобках - это указатели на тройки, а имена - это
указатели на таблицу символов. На практике информация,
необходимая для интерпретации различного типа входов в поля
arg1 и arg2, кодируется в поле op или дополнительных полях.
Тройки рис. 4.4.б соответствуют четверкам рис. 4.4.а.
+----------------------------++------------------------+
| |op |arg1 | arg2 |result|| | op | arg1 | arg2 |
|---+----+-----+------+------||-----+----+------+------|
|(0)| - | c | | t1 || (0) | - | c | |
|(1)| * | b | t1 | t2 || (1) | * | b | (0) |
|(2)| - | c | | t3 || (2) | - | c | |
|(3)| * | b | t3 | t4 || (3) | * | b | (2) |
|(4)| + | t2 | t4 | t5 || (4) | + | (1) | (3) |
|(5)| := | t5 | | a || (5) | := | a | (4) |
+----------------------------++------------------------+
а) четверки б) тройки
Рис. 4.4
Для представления тройками трехместной операции типа x[i]:=y
требуется два входа, как это показано на рис. 4.5.а,
представление x:=y[i] двумя операциями показано на рис. 4.5.б.
+------------------------+ +-------------------------+
| | op | arg1 | arg2 | | | op | arg1 | arg2 |
|----+-----+------+------| |-----+-----+------+------|
|(0) | []= | x | i | | (0) | =[] | y | i |
|(1) | := | (0) | y | | (1) | := | x | (0) |
+------------------------+ +-------------------------+
а) x[i]:=y б) x:=y[i]
Рис. 4.5
Трехадресный код может быть представлен не списком троек, а
списком указателей на них. Такая реализация обычно называется
косвенными тройками. Например, тройки рис. 4.4.б могут быть
реализованы так, как это изображено на рис. 4.6.
+---------------+ +------------------------+
| | оператор | | | op | arg1 | arg2 |
|----+----------+ |-----+----+------+------|
|(0) | (14) | | |(14) | - | c | |
|(1) | (15) | | |(15) | * | b | (14) |
|(2) | (16) | | |(16) | - | c | |
|(3) | (17) | | |(17) | * | b | (16) |
|(4) | (18) | | |(18) | + | (15) | (17) |
|(5) | (19) | | |(19) | := | a | (18) |
+---------------+ +------------------------+
Рис. 4.6
При генерации объектного кода каждой переменной, как
временной, так и определенной в исходной программе,
назначается память периода исполнения, адрес которой обычно
хранится в таблице генератора кода. При использовании четверок
этот адрес легко получить через эту таблицу.
Более существенно преимущество четверок проявляется в
оптимизирующих компиляторах, когда может возникнуть
необходимость перемещать операторы. Если перемещается
оператор, вычисляющий x, не требуется изменений в операторе,
использующем x. В записи же тройками перемещение оператора,
определяющего временное значение, требует изменения всех
ссылок на этот оператор в массивах arg1 и arg2. Из-за этого
тройки трудно использовать в оптимизирующих компиляторах.
В случае применения косвенных троек оператор может быть
перемещен переупорядочиванием списка операторов. При этом не
надо менять указатели на op, arg1 и arg2. Этим косвенные
тройки похожи на четверки. Кроме того, эти два способа требуют
примерно одинаковой памяти. Как и в случае простых троек, при
использовании косвенных троек выделение памяти для временных
значений может быть отложено на этап генерации кода. По
сравнению с четверками при использование косвенных троек можно
сэкономить память, если одно и то же временное значение
используется более одного раза. Например, на рис. 4.6 можно
объединить строки (14) и (16), после чего можно объединить
строки (15) и (17).
4.3. Линеаризованные представления
В качестве промежуточных представлений весьма распространены
линеаризованные представления. Линеаризованное представление
позволяет относительно легко хранить промежуточное
представление на внешней памяти и обрабатывать его в порядке
чтения. Самая распространенная форма линеаризованного
представления - это запись дерева либо в порядке его обхода
снизу-вверх (постфиксная запись, или обратной польской), либо
в порядке обхода его сверху-вниз (префиксная запись, или
прямой польской).
Таким образом, постфиксная запись (обратная польская) - это
список вершин дерева, в котором каждая вершина следует
непосредственно за своими потомками. Дерево на рис. 4.1 в
постфиксной записи может быть представлено следующим образом:
a b c - * b c * + :=
В постфиксной записи вершины синтаксического дерева явно не
присутствуют. Они могут быть восстановлены из порядка, в
котором следуют вершины и из числа операндов соответствующих
операций. Восстановление вершин аналогично вычислению
выражения в постфиксной записи с использованием стека.
В префиксной записи сначала указывается операция, а затем ее
операнды. Например, для приведенного выше выражения имеем
:= a + * b - c * b - c
Рассмотрим детальнее одну из реализаций префиксного
представления - Лидер [4]. Лидер - это аббревиатура от
'ЛИнеаризованное ДЕРево'. Это машинно-независимая языково-
ориентированная префиксная запись. В этом представлении
сохраняются все объявления и каждому из них присваивается свой
уникальный номер, который используется для ссылки на
объявление. Рассмотрим пример (рис. 4.7).
module M;
var X,Y,Z: integer;
procedure DIF(A,B:integer):integer;
var R:integer;
begin R:=A-B;
return(R);
end DIF;
begin Z:=DIF(X,Y);
end M.
Рис. 4.7
Соответствующий образ в Лидере изображен на рис. 4.8.
program 'M'
var int
var int
var int
procbody proc int int end int
var int
begin assign var 1 7 end
int int mi par 1 5 end par 1 6 end
result 0 int var 1 7 end
return
end
begin assign var 0 3 end int
icall 0 4 int var 0 1 end int var 0 2 end end
end
Рис. 4.8
Рассмотрим его более детально:
program 'M' Имя модуля используется для редактора
связей
var int Это образ переменных var X,Y,Z:integer;
var int переменным X,Y,Z присваиваются номера
var int 1,2,3 на уровне 0
procbody proc Объявление процедуры с двумя
int int end целыми параметрами, возвращающей целое.
int Процедура получает номер 4 на уровне 0 и
параметры имеют номера 5, 6 на уровне 1.
var int Локальная переменная R имеет номер 7 на
уровне 1
begin Начало тела процедуры
assign Оператор присваивания
var 1 7 end Левая часть присваивания (R)
int Тип присваиваемого значения
int mi Целое вычитание
par 1 5 end Уменьшаемое (A)
par 1 6 end Вычитаемое (B)
result 0 Результат процедуры уровня 0
int Результат имеет тип целый
var 1 7 end Результат - переменная R
return Оператор возврата
end Конец тела процедуры
begin Начало тела модуля
assign Оператор присваивания
var 0 3 end Левая часть - переменная Z
int Тип присваиваемого значения
icall 0 4 Вызов локальной процедуры DIF
int var 0 1 end Фактические параметры X и Y
int var 0 2 end
end Конец вызова
end Конец тела модуля
4.4. Организация информации в генераторе кода
Чисто синтаксическое дерево несет только информацию о
структуре программы. На самом деле в процессе генерации кода
требуется также информация о переменных (например, их адреса),
процедурах (также адреса, уровни), метках и т.д. Для
представления этой информации возможны различные решения.
Наиболее распространены два. 1) всю эту информацию можно
хранить в таблицах генератора кода; 2) информация хранится в
вершинах дерева с соответствующими указателями.
Рассмотрим, например, структуру таблиц, которые могут быть
использованы в сочетании с Лидер-представлением. Поскольку
Лидер-представление не содержит информации об адресах
переменных, значит, эту информацию нужно формировать в
процессе обработки объявлений и хранить в таблицах. Это
касается и описаний массивов, записей и т.д. Кроме того, в
таблицах также должна содержаться информация о процедурах
(адреса, уровни, модули, в которых процедуры описаны, и т.д.).
Таким образом структура таблиц может быть такой, как это
изображено на рис. 4.9.
Таблица уровней Таблица описаний
процедур
+-----+ +----------------------------+
| --+--------+ |Для типа:размер |
|-----| | |----------------------------|
| --+----+ | |Для переменной:указатель на |
| ....|.. | | |тип и адрес (смещение) |
|-----| | | |----------------------------|
| --+--+ | +-->|Для процедуры: адрес, ... |
+-----+ | | |----------------------------|
| +--> |............................|
| | |
| |----------------------------|
+-------->| |
+----------------------------+
Рис. 4.9
При входе в процедуру в таблице уровней процедур заводится
новый вход - указатель на таблицу описаний. При выходе
указатель восстанавливается на старое значение. Если
промежуточное представление - дерево, то информация может
храниться в вершинах самого дерева. Например, та же информация
может быть представлена, как на рис. 4.10.
/\
/ \
/ \
/ \
/ \
Раздел описаний / \
/ \ \
/ \ \
/ \ \
Описание Описание Раздел
типа переменной операторов
+--------+ +--------+ +------+
| |<--+--- |<----------+--- |
+--------+ +--------+ +------+
Рис. 4.10
4.5. Уровень промежуточного представления
Как видно из приведенных примеров, промежуточное представление
программы может в различной степени быть близким либо к
исходной программе, либо к машине. Например, промежуточное
представление может содержать адреса переменных, и тогда оно
уже не может быть перенесено на другую машину. С другой
стороны, промежуточное представление может содержать раздел
описаний программы, и тогда информацию об адресах можно
извлечь из обработки описаний. В то же время ясно, что первое
более эффективно, чем второе.
Операторы управления в промежуточном представлении могут
быть представлены в исходном виде (в виде операторов языка if,
for, while и т.д.), а могут содержаться в виде переходов. В
первом случае некоторая информация может быть извлечена из
самой структуры (например, для оператора for - информация о
переменной цикла, которую, может быть, разумно хранить на
регистре, для оператора case - информация о таблице меток и
т.д.). Во втором случае представление проще и унифицированней.
Некоторые формы промежуточного представления лучше годятся
для различного рода оптимизаций (например, косвенные тройки -
для перемещения кода), некоторые - хуже (например, префиксная
запись для этого плохо подходит).
|