Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

metoda_2013

.pdf
Скачиваний:
54
Добавлен:
03.05.2015
Размер:
6.36 Mб
Скачать

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

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

Из-за этих проблем механизм счетчиков ссылок не получил широкого распространения.

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

Разметка памяти начинается с обнаружения всех заведомо живых элементов программы. К таким причисляются все объекты за пределами кучи (на стеке, в регистрах процессора и т.д.), а также все объекты в куче, на которые они указывают. Все эти элементы помечаются как используемые. Затем мы перебираем все используемые элементы и помечаем все прочие объекты, на которые они ссылаются. Этот процесс повторяется рекурсивно до тех пор, пока мы не перестаем находить новые используемые элементы.

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

16.Распределение памяти. Адреса времени компиляции.

Кадресам времени компиляции относятся: адреса процедур, меток, другие любые адреса в исполняемом коде, а также статические переменные и массивы. При компиляции вызова процедуры или goto на метку просто генерируется переход (или call) на метку, объявленную в таблице символов. При неявных переходах (например, оператор break или continue в цикле) создаются метки для начала и конца цикла, на них и происходит переход.

290

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

Вобщем случае, в процессе компиляции адреса переменных неизвестны. Укажем некоторые причины, почему это происходит.

o Во время выполнения программы расположение стекового фрейма, который соответствует конкретной функции или процедуре, зависит от порядка вызова функций (процедур).

o В процессе компиляции значение индексов массива обычно неизвестно и будет вычисляться при выполнении программы.

o Доступ к некоторым переменным осуществляется посредством указателей, значения которых в процессе компиляции неизвестны.

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

o Смещение простого значения относительно основания стекового фрейма.

o Смещение начала массива относительно основания стекового фрейма.

o Статическая глубина функции, в которой объявлена переменная. Статическая глубина (пункт 3) относится к языкам Рascal и Аdа (в С такого понятия нет).

Вязыке С адрес простой переменной в процессе компиляции представляет собой смешение по отношению к основанию стекового фрейма. Это же относится и к полю записи, так как поля записи всегда запоминаются последовательно, и предполагается, что объем требуемой памяти для каждого из полей известен. Для языка Рascal или Аdа адрес времени компиляции простой переменной или поля записи будет состоять из пары: (номер уровня, офсет)

Здесь номер уровня — номер статического уровня функции или процедуры, в котором была объявлена переменная или запись, а термин "офсет" употребляется с тем же значением, что и в языке С (смещение от начала фрейма).

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

o Смешение начала массива по отношению к

основанию

стекового

фрейма.

 

291

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

o Смещение элемента массива по отношению к началу массива.

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

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

Рассмотрим объявление массива.

vаr table: аггау [1..10,1. .20] оf integer

Элементы массива обычно записывают построчно или, точнее, согласно лексикографическому порядку индексов. Например, значения элементов приведенной таблицы будут

занесены в память в следующем порядке.

 

table

[1,1],

table

[1,2],.. table

[1,20],

table [2,1], table [2,2], table [2,20],

 

table

[10,1], table

[10,2] table

[10,20]

 

Адрес конкретного элемента массива вычисляется как смещение от адреса первого элемента массива.

адрес(table [i, j]) = адрес (table [l1, l2]) + (U2-l2+1)* (i-l1) + (j

– l2)

Здесь l1, и u1, — нижняя и верхняя границы первого измерения и т.д., а каждый элемент массива предполагается размером в одну ячейку памяти. В приведенном выше примере нижние границы в каждом случае равны 1, а верхние — 10 и 20 соответственно. Для трехмерного массива аrr3, объявленного как

Vаr Arr: аrrау [1..10, 1..10, 1..10] оf integer;

общая формула для адреса элемента массива arr3 [x,y,z] имеет следующий вид.

офсет (arr[i,j,k]) = адрес(arr [l1,l2 ,l3]) + (u2 – l2 + 1 )* (u3 – l3 +1) * (i - l1)+ (u3-l3+1) *(j-l2) + (k-l3)

Из приведенной выше формулы для нахождения смещения элемента массива относительно адреса первого элемента массива понятно, что вычисления становятся достаточно простыми, если известны шаги по индексам. Например, для агг адрес элемента arr[i,j,k] выражается следующим образом:

адрес(arr[i,j,k]) = адрес(arr [l1,l2 ,l3]) + (u2 – l2 + 1 )* (u3 – l3 +1) * (i - l1) + (u3-l3+1) *(j-l2) + (k-l3)

292

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

S1, S2, S3 — шаги по соответствующим индексам, равные следующему.

(u2 – l2 + 1 )* (u3 – l3 +1) (u3-l3+1)

1

Для языков, в которых границы массива известны во время компиляции, значения шагов по индексам могут вычисляться сразу же (во время компиляции), что сокращает количество вычислений времени выполнения программы при каждом обращении к массиву. В то же время, дальше упростить приведенную формулу уже невозможно, поскольку разность (i- l1,), в общем случае, во время компиляции неизвестна. Для языков с динамическими границами (до выполнения программы они неизвестны) шаги по индексам можно найти после объявления массива и занесения его в стек, что опять же уменьшит количество вычислений, выполняемых при каждом обращении к массиву. Хотя значения шагов по индексам в процессе компиляции могут быть неизвестны, практически всегда будет известен объем памяти, которую будут занимать шаги по индексам, и память для них может быть выделена в процессе компиляции. В то же время память для самих элементов массива может выделяться только при выполнении программы, поскольку при компиляции значения границ могут быть неизвестны.

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

o Все простые значения (типы integer, float и т.д.).

oСтатические части массивов (границы, шаги по индексам, указатели на элементы массива).

oСтатические части записей (поля, размеры которых известны во время компиляции).

oУказатели на глобальные значения — хотя глобальные значения будут храниться не в стеке, а в куче.

293

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

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

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

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

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

294

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

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

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

Для этого компилятор должен последовательно выполнить следующие задачи:

выделить память под переменную;

инициализировать выделенную память

некоторым начальным значением;

предоставить программисту возможность использования этой памяти;

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

наконец, необходимо обеспечить возможность

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

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

С точки зрения программиста, память становится свободной как только выполняется оператор явного освобождения памяти (free/delete) или в момент окончания времени жизни последней переменной, использующей данную область памяти. Эти операции, делающие структуру данных логически недоступной, называются уничтожением памяти. Затем освобожденную память необходимо вернуть системе

295

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

как свободную — утилизировать. Операции уничтожения памяти и утилизации могут быть сильно разнесены по времени.

При создании компилятора необходимо различать два важных класса информации:

Статическая информация, т.е. информация, известная во время компиляции

Динамическая информация, т.е. сведения, неизвестные

во время компиляции, но которые станут известны во время выполнения программы

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

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

Можно выделить три основных метода управления памятью:

Статическое распределение памяти

Стековое распределение памяти

Представление памяти в виде кучи (heap) Статическое управление памятью представляет собой

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

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

296

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

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

Адреса переменных, в общем случае, в процессе компиляции неизвестны, т.к.:

1.Во время выполнения программы расположение стэкового фрейма зависит от порядка вызова функций, а это не всегда известно

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

3.Доступ к некоторым переменным – через указатели, значение которых также неизвестно в процессе компиляции:

1.Смещение простого значения относительно основания стэкового фрейма

2.Смещение основания массива относительно

основания стэкового фрейма В принципе, для простой переменной в процессе

компиляции адрес представляется как смещение к основанию стэка. Это же относится и к полям записи и предполагает, что объем каждого из полей известен.

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

LINE array [l..n] of int;

Хотим найти: LINE [i] = 5;

ADDR(LINE) – адрес относительно начала стекового фрейма

ADDR(LINE[i]) = ADDR(LINE) + (i - l);

MATRIX array [l1..n1, l2..n2] of int; Хотим найти: MATRIX [i, j] = 5;

ADDR(MATRIX) – адрес относительно начала стекового фрейма

ADDR(MATRIX [i, j]) = ADDR(MATRIX) + (i – l1)(n2 – l2) + (j-l2) = ADDR(MATRIX) + i(n2 – l2) + j – l1(n2 - l2) – l2;

297

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

Выделим -l1(n2 - l2) – l2 – в константу с, а (n2 – l2) – в константу s:

ADDR(MATRIX [i, j]) = ADDR(MATRIX) + i*s + j + c

Объемный массив:

MATRIX array [l1..n1, l2..n2, l3//n3] of int; Хотим найти: MATRIX [i, j, k] = 5;

ADDR(MATRIX) – адрес относительно начала стекового фрейма

ADDR(MATRIX [i, j, k]) = ADDR(MATRIX) + (i – l1)(n2 – l2)(n3 - l3) + (j-l2)(n3 - l3) + (k-l3)

Аналогично предыдущему, можно выделить константы.

При динамическом массиве – массиве с динамическими границами – потребуется более обобщенная модель стека выполнения: выделим 2 части памяти: стековая статическая часть и стековая динамическая часть. Все значения динамической части можно получить через указатель и значение статической части.

Следовательно, в статической части:

1.Известные значения, простые типы (float, int …)

2.Границы, шаги, указатели на элементы массива, статические записи

3.Указатель на глобальные значения

Вдинамической части будут находиться элементы массива (элементы статич. массива). Для доступа к элементам необходимо знать смещение относительно динамической части.

18. Генерация кода. Генерация кода на примере одного из

операторов Паскаля.

Задача генератора кода - построение для программы на входном языке эквивалентной машинной программы. Обычно в качестве входа для генератора кода служит некоторое промежуточное представление программы.

На этапе генерации кода происходит замена операторов языка высокого уровня инструкциями ассемблера, а затем последовательностями машинных команд. Результат преобразования исходного текста программы записывается в виде двоичного файла (его называют объектным модулем) с расширением ".obj". На этой стадии внутренний код, сгенерированный семантическим анализатором, заменяется на объектный.

298

ТЕОРИЯ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ И МЕТОДЫ ТРАНСЛЯЦИИ

Генерация кода включает ряд специфических, относительно независимых подзадач: распределение памяти (в частности, распределение регистров), выбор команд, генерацию объектного (или загрузочного) модуля. Конечно, независимость этих подзадач относительна: например, при выборе команд нельзя не учитывать схему распределения памяти, и, наоборот, схема распределения памяти (регистров, в частности) ведет к генерации той или иной последовательности команд. Однако удобно и практично эти задачи все же разделять, обращая при этом внимание на их взаимодействие.

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

Представление в виде ориентированного графа

Простейшей формой промежуточного представления является синтаксическое дерево программы. Более полную информацию о входной программе дает ориентированный ациклический граф (ОАГ), в котором в одну вершину объединены вершины синтаксического дерева, представляющие одни и те же константы, переменные и общие подвыражения. На рис. 1 приведены оба ПП программы.

Рис. 1. Промежуточные представления программы.

Трехадресный код Трехадресный код - это последовательность операторов

вида x:= y op z, где x,y и z - имена, константы или сгенерированные компилятором временные объекты. Здесь op - двуместная операция, например операция плавающей или фиксированной арифметики, логическая или побитовая. В правую часть может входить только один знак операции.

299

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]