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

191

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

struct

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

int

i

 

 

struct

 

 

 

real

r

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис

int

J

 

bool

y

 

 

 

 

7.4. Структура типа struct

 

 

 

Каждая ячейка имеет два (возможно пустых) указателя; вертикальный указатель используется в случае структурных, объединенных и процедурных видов.

С помощью рассмотренного метода представления видов компилятор может легко выполнять следующие операции:

1)нахождение вида результата процедуры;

2)выбор структуры поля;

3)разыменование значения, т.е. замену адреса значением в адресе;

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

Контрольные вопросы

1.Понятие прохода компилятора, необходимость использования нескольких проходов при компиляции.

2.Назначение таблицы символов.

3.Методы организации таблицы символов.

4.Хеширование, разрешение конфликтов при хешировании.

5.Сцепление элементов и бинарное дерево.

6.Назначение таблицы видов.

7.Способы организации таблицы видов.

9.Распределение памяти

9.1. Стек времени прогона

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

192

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

Каждой программе для хранения значений переменных и промежуточный значений выражений необходим определенный объем памяти. Например, если идентификатор описывается как

int x,

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

делить достаточно места, чтобы записать любое целое

число.

Аналогично, если у описывается как

 

 

 

 

struct (int number, real size, bool n) y,

 

 

то компилятор обеспечивает для значенияу память с объемом, доста-

точным для хранения в нем целого, вещественного и логического зна-

чения. В

обоих

случаях

компилятор

не

должен

испытывать

затруднений в вычислении требуемого объема памяти. Если z описывается

int z[10],

то объем памяти, необходимый для хранения всех элементов z, в 10 раз больше памяти для записи одного целого значения. Однако, если бы w был описан в виде

int w[n],

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

Память нужна также для промежуточных результатов и констант. Например, при вычислении выражения a + c ´ d сначала вычисляется c ´ d, причем значение запоминается в машине, а затем выполняется сложение. Память, используемая для хранения результатов, называется рабочей. Рабочая память может быть статической и динамической.

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

193

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

INTEGER A, B, X, Y

выделяется память, как это показано на рис. 8.1.

A B X Y

Рис. 8.1. Распределение памяти для Фортрана

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

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

Пусть имеется программа вида: begin real x, y

.

.

.

begin int c, d

.

.

.

end

begin char p, q

.

.

.

end

.

.

.

end

На рис. 8.2 показаны «моментальные снимки» стека времени прогона на различных этапах ее выполнения.

 

 

194

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

d

 

 

q

 

 

 

c

 

 

p

y

 

y

 

 

y

x

 

x

 

 

x

(1)

 

 

(2)

 

 

 

(3)

 

 

 

Рис. 8.2. Стек времени прогона

 

 

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

Кроме указателя стека требуется также указатель на дно текущей рамки (указатель рамки). При входе в блок этот указатель устанавливается равным текущему значению указателя стека. При выходе из блока сначала указатель стека устанавливается равным значению, соответствующему включающему блоку. Указатель рамки включающего блока может храниться в нижней части текущей рамки стека, образуя часть статической цепи или массива, который называется дисплеем. Его можно использовать для хранения во время прогона указателей на начало рамок стека, соответствующих всем текущим блокам (рис. 8.3).

q

p

x

ДИСПЛЕЙ y СТЕК

Рис. 8.3. Система «дисплей-стек»

Это упрощает настройку указателя при выходе из блока.

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

Рассмотрим пример программы: begin int n; read(n);

int numbers[n]; real p;

begin real x, y;

Место для numbers должно выделяться в первой рамке стека, а для х и у – в рамке над ней. Но во время компиляции неизвестно, где должна начинаться вторая рамка, так как неизвестен размер чисел. Одно из решений в этой ситуации – иметь два стека: один для статиче-

195

ской памяти, распределяемой в процессе компиляции, а другой для динамической памяти, распределяемой в процессе прогона.

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

 

 

Рамка

у

 

2

х

 

 

 

Дина-

 

Числа

миче-

 

ская

 

 

 

 

часть

Рамка

 

 

1

p

Ста-

 

nтическая

часть

Дисплей Стек

Рис. 8.4. Стек прогона для массива В этой структуре массив занимает только динамическую память. Од-

нако некоторая информация о массиве известна во время компиляции, например его размерность (а, следовательно, и число границ – две на каждую размерность), и при выборке определенного элемента массива она может потребоваться. В некоторых языках сами границы могут быть неизвестны при компиляции, но всегда известно их число, и для значений этих границ можно выделить статическую память. Тогда можно считать, что массив состоит из статической и динамической частей. Статическая часть массива может размещаться в статической части рамки, а динамическая – в динамической. Кроме информации о границах, в статической части может храниться указатель на сами элементы массива (рис. 8.5).

 

 

196

 

 

 

 

 

 

 

Рамка

 

 

 

 

 

 

 

у

 

 

2

 

 

х

 

 

 

 

 

Эле-

 

Дина-

 

 

 

мен-

 

 

 

 

 

миче-

 

 

 

ты

 

 

 

 

 

ская

Рамка

 

 

чисел

 

 

 

 

часть

1

 

 

 

 

 

 

 

 

Ста-

 

 

 

p

 

 

 

 

 

 

тиче

 

 

 

Стати-

 

ская

 

 

 

ческая

 

часть

 

 

 

часть

 

 

 

 

 

чисел

 

 

 

 

 

n

 

 

 

 

 

 

 

 

 

Дисплей

 

Стек

 

 

 

Рис. 8.5. Модифицированный стек прогона для массива

Когда в программе выбирается конкретный элемент массива, его адрес внутри динамической памяти должен вычисляться в процессе прогона. Пусть имеется массив

int table[1:10, -5:5].

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

table[1, -5], table[1, -4]………. table[1, 5], table[2, -5], table[2, -4]………. table[1, 5],

.

.

.

table[10, -5], table[10, -4]………. table[10, 5].

Адрес конкретного элемента вычисляется как смещение по отношению к базовому адресу (адресу первого элемента) массива:

ADDR(table[I , J ]) = ADDR(table[l1, l2 ])+ (u2 - l2 +1)´ (I - l1 )+ (J - l2 ).

Здесь l1 и u1 - нижняя и верхняя границы первой размерности и т.д. и считается, что элемент массива занимает единицу объема памяти.

Выражение (ui - li +1) задает число различных значений, которые может принимать i-й индекс. Расстояние между элементами, отли-

197

чающимися на единицу в i-м индексе, называется i-й шагом и обозначается si . Пример шагов массива (рис. 8.6):

 

int N[1:5, 1:5, 1:5]

N[1,1,1]

N[1,1,2]..N[1,1,5] N[1,2,1]… N[2,1,1] …N[5,5,5]

 

s1

 

 

 

 

 

 

 

 

 

 

 

 

 

s2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

s3

 

 

 

 

 

 

 

Рис. 8.6. Схема смещений

 

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

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

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

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

x=a+b´ c.

При вычислении выражения (a+b´ c) потребуется рабочая память, чтобы записать b´ c перед сложением. Ту же самую рабочую память можно использовать для хранения результатов сложения. Однако после осуществления операции присвоения этот объем памяти можно освободить, так как он уже не нужен.

Динамическая память должна распределяться во время прогона, статическая же распределяется во время компиляции. Объем статической рабочей памяти, который должен выделяться каждой рамке, оп-