Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Теория языков программирования методов трансляции.-1.pdf
Скачиваний:
13
Добавлен:
05.02.2023
Размер:
1.36 Mб
Скачать

120

 

 

 

 

*

 

 

 

 

*

 

 

 

*

F

 

 

 

 

 

 

*

 

E

 

 

 

 

 

-

 

D

 

 

 

 

 

B

 

A

 

 

 

 

 

 

 

C

 

 

 

Рис.11.6. Граф ¢.

6.2. Арифметические выражения

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

Желательно, чтобы результирующая программа на языке ассемблера была бы «хорошей» относительно некоторой оценки, такой, например, как число команд ассемблера или число обращений к памяти.

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

Блок, представляющий одно выражение, имеет только одну выходную переменную. Ограничение, состоящее в том, что выражение не имеет одинаковых операндов, эквивалентно требованию, чтобы граф для этого выражения был деревом.

Для удобства будем предполагать, что все операции бинарные. Код на языке ассемблера будем генерировать для машины сN сумматорами, где N ³ 1. Оценкой будет длина программы (т.е. число команд).

6.2.1.Модель машины

121

Рассмотрим машину сN ³ 1 универсальными сумматорами и командами четырех типов.

Определение. Командой языка ассемблера называется цепочка символов одного из четырех типов:

LOAD

M, A

STORE A, M

OP q

A, M, B

OP q

A, B, C.

В этих командах M – ячейка памяти, A, B, C – имена сумматоров (возможно одинаковые). OP q - это код бинарной операции q. Предполагается, что каждой операции q соответствует машинная команда3) или 4). Эти команды выполняют следующие действия.

1)LOAD M, A помещает содержимое ячейки памятиM на сумматор A.

2)STORE A, M помещает содержимое сумматора A в ячейку па-

мяти M.

3)OP q A, M, B применяет бинарную операцию q к содержимому сумматора A и ячейки памяти M, а результат помещает на сумматор B.

4)OP q A, B, C применяет бинарную операцию q к содержимому сумматоров A и B, а результат помещает на сумматор C.

Программой на языке ассемблера будем называть последовательность команд языка ассемблера.

Если P = I1; I2; ; In – программа, то можно определить значение vt(R) регистра R последней команды t (под регистром понимается сумматор или ячейка памяти):

1)v0(R) равно R если R – это ячейка памяти, и не определено, если R – сумматор,

2)если It – это LOAD M, A, то vt(A) = vt-1(M),

3)если It – это STORE A, M, то vt(M) = vt-1(A),

4)если It – это OP q A, B, C, то vt(С) = q vt-1(A) vt-1(R),

5)если vt(R) не определено по2) – 4), а vt-1(R) определено, то

vt(R) = vt-1(R); в противном случае vt(R) не определено.

Команды LOAD и STORE передают значения с одного регистра на другой, оставляя их в исходном регистре. Операции перемещают вычисленное значение на сумматор, определенный третьим аргументом, не меняя остальных регистров. Будем говорить, что программа P вычисляет выражение a, помещая результат на сумматорA, если после последнего оператора из P сумматор A имеет значение a.

Пример. Рассмотрим программу на языке ассемблера с двумя сумматорами A и B, значения которых после каждой команды приведены в табл. 11.3.

 

122

 

Таблица 11.3.

 

 

 

v(A)

v(B)

LOAD X, A

X

не определено

ADD A, Y, B

X + Y

не определено

LOAD Z, B

X + Y

Z

MULT B, A, A

Z * (X +Y)

Z

Значение сумматора A в конце программы соответствует выражению Z*(X +Y). Таким образом, эта программа вычисляетZ * (X +Y), помещая результат на сумматор A.

Формально определим синтаксическое дерево как помеченное двоичное дерево T, имеющих одну или более таких вершин, что

1)каждая внутренняя вершина помечена бинарной операцией

qÎQ,

2)все листья помечены различными именами переменных XÎS. Будем предполагать, что множества Q и S не пересекаются. Вер-

шинам дерева, начиная снизу, можно следующим образом приписать

значения:

1)если n – лист, помеченный X, то n имеет значение X,

2)если n – внутренняя вершина, помеченная q, и ее прямыми по-

томками являются n1 и n2 со значениями v1 и v2, то n имеет значение q v1 v2.

Значением дерева будем считать значение его корня.

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

6.2.2.Разметка дерева

Алгоритм разметки синтаксического дерева.

Вход. Синтаксическое дерево T.

Выход. Помеченное синтаксическое дерево.

Метод. Вершинам дерева T рекурсивно, начиная снизу, назначаем целочисленные метки:

1)Если вершина есть лист, являющимся левым прямым потомком своего прямого предка, или корень, помечаем вершину 1; если вершина есть лист, являющимся правым потомком, помечаем ее 0.

2)Если вершина n имеет прямых потомковn1 и n2, помеченных l1, l2. Если l1,= l2. берем в качестве метки число l1 +1.

Пример. Арифметическое выражение

A*(B-C)/D*(E-F)

123

изображенное в виде дерева на рис. 11.7, на котором представлены целочисленные метки.

/3

*

2

*

2

A 1

1 -

D 1

- 1

B 1

0 C

E 1

F 0

Рис. 11.7. Помеченное синтаксическое дерево.

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

Алгоритм построения кода на языке ассемблера для выражений.

Вход. Помеченное синтаксическое дерево T и N сумматоров A1, A2,

…, AN, где N ³ 1.

Выход. Программа P на языке ассемблера, после последней команды которой значением v(A1) становится v(T); т.е. P вычисляет выражение, представленное деревом T, и помещает результат на сумматор A1.

Метод. Предполагается, что дерево T помечено в соответствии с алгоритмом разметки синтаксического дерева. Затем рекурсивно выполняется процедура code(n, i). Входом для code служит вершина n дерева T и целое число i от 1 до N. Число i означает, что в данный момент для вычисления выражения доступны сумматорыAi, Ai+1, …, AN. Выходом code(n, i) служит последнее значениеv(n) и помешает результат на сумматор Ai.

Сначала выполняется code(n0, 1), где n0 – корень дерева T. Последовательность команд, генерированных этим вызовом процедуры code, и будет нужной программой на языке ассемблера.

124

Процедура code(n, i). Предполагается, что n - вершина дерева T, а i - целое число между 1 и N.

1)Если n – лист, выполняем шаг 2). В противном случае выполняем шаг 3).

2)Если вызвана процедура code(n, i), а n – лист, то n всегда будет левым прямым потомком (или корнем, если n – единственная вершина дерева). Если с листом n связано имя переменной X, то

code(n, i) = ‘LOAD X, Ai

(это означает, что выходом процедуры code(n, i) служит команда

LOAD X, Ai).

3) В это место мы приходим, только если n – внутренняя вершина. Пусть с вершиной n операция q и ее прямыми потомками являются n1 и n2 с метками l1 и l2.

q

l1

n1

n2

l2

 

 

 

Следующий шаг определяется значением меток l1 и l2: (а) если l2 = 0 (n2 – правый лист), выполняем шаг 4), (б) если 1 £ l1 < l2 и l1 < N, выполняем шаг 5),

(в) если 1 £ l2 £ l1 и l2 < N, выполняем шаг 6), (г) если N £ l1 и N < l2, выполняем шаг 7).

4) code(n, i) = code(n1, i) OP q Ai, X, Ai

Здесь X – переменная, связанная с листом n2, а OP q - код операции q. Выходом для code(n, i) служит выход для code(n1, i), сопровождаемый командой OP q Ai, X, Ai.

5) code(n, i) = code(n2, i) code(n1, i+1)

OP q Ai+1, Ai, Ai

6) code(n, i) = code(n1, i) code(n2, i+1)

OP q Ai, Ai+1, Ai

125

7) code(n, i) = code(n2, i)

T ¬ newtemp

‘STORE Ai, T code(n1, i)

OP q Ai, T, Ai

Здесь newtemp – функция, которая при обращении к ней вырабатывает новую временную ячейку памяти для запоминания промежуточных результатов.

Пример. Применим алгоритм при N =2 к синтаксическому дереву на рис. 11.6. Последовательность вызовов code(n, i) приведена в табл. 11.4.

Таблица 11.4.

Вызов

Шаг

code(/, 1)

(3г)

code(*R, 1)

(3в)

code(D, 1)

(2)

code(-R, 2)

(3а)

code(E, 2)

(2)

code(*L, 1)

(3в)

code(A, 1)

(2)

code(-L, 2)

(3а)

code(B, 2)

(2)

Здесь *L – ссылка на левый потомок вершины /, *R – на правый потомок вершины *L, а R на правый потомок вершины *R.

Процедура code(/, 1) генерирует программу

LOAD D, A1

LOAD E, A2 SUBTR A2, F, A2 MULT A1, A2, A1 STORE A1, TEMP1

LOAD A1, A1

LOAD B, A2 SUBTR A2, C, A2 MULT A1, A2, A1

DIV A1, TEMP1, A1

Рассмотренные нами алгоритмы позволяют сформулировать две базовые теоремы для построения оптимального кода.

Теорема 1. Программа, выработанная процедурой code(n, i) правильно вычисляет значение вершины n, помещая его на i-й сумматор.