Синтаксический анализ
Лексический анализ
Семантический анализ
Генерация промежуточного кода
Оптимизация промежуточного кода
Генерация внутреннего представления
Компоновка программы
Фазы компиляции
Выполнение и тестирование программы
Выход
Определение
Входом компилятора служит программа на исходном языке программирования. С точки зрения компилятора это просто последовательность символов. Задача первой фазы компиляции, лексического анализатора (lexical analysis) , заключается в разборе входной цепочки и выделении некоторых более "крупных" единиц, лексем, которые удобнее для последующего разбора. Примерами лексем являются основные ключевые слова, идентификаторы, константные значения (числа, строки, логические) и т.п.
На этапе лексического анализа обычно также выполняются такие действия, как удаление комментариев и обработка директив условной компиляции.
Пример 1:
Лексический анализ фрагмента программы на Турбо-Паскале, содержащего только один правильно записанный оператор ввода:
read (NABOR,CHISLO);
дает цепочку
1 2 3 4 3 2 4
где 1 – код названия оператора;
2 – код скобки;
3 – код имени;
4 – код знака препинания.
Пример 2:
Лексический анализ фрагмента программы на Турбо-Паскале, содержащего только один неправильно записанный оператор ввода:
rread (NABOR,CHISLO);
дает цепочку
3 2 3 4 3 2 4
В то же время структура "правильного" оператора ввода определяется схемой:
Поскольку представленные структуры идентичны, оператор read (NABOR,CHISLO); при синтаксическом анализе определяется как правильный.
Рис 1
Рис 2
Синтаксический анализ
Именно на этом шаге выявляются ошибки в написании названий операторов: они ведут к некорректной структуре всего оператора. Так, примеру с "неправильным" оператором ввода соответствует структура, показанная на рисунке: (рис. 3)
Рис 3
Рис 4
Поскольку структура этого рисунка не соответствует структуре рисунка 1, оператор rread (NABOR,CHISLO); расценивается как синтаксически некорректный: компилятор не найдет в анализируемом фрагменте требуемого действия, которое задается названием оператора, и сообщит об этом программисту.
После синтаксического анализа можно считать, что исходная программа преобразована в некоторое промежуточное представление. Как некоторую форму промежуточного представления рассмотрим дерево разбора (рис. 4) . В дереве разбора программы внутренние узлы соответствуют операциям, а листья представляют операнды.
Синтаксический анализ
На этом шаге выявляются ошибки, допущенные программистом в нарушение правил составления программ, например, следующего вида: все переменные и константы перед употреблением в операторах языка должны быть описаны; каждое имя (переменной или константы) должно быть описано только один раз; требуется согласование типов переменных с использующими их функциями и т.д. Так, если вся программа состоит только из оператора
read (NABOR,CHISLO
она будет определена как семантически некорректная, поскольку в ней отсутствуют описания вводимых переменных.
Пример 4:
Программа
var CHISLO: integer;
CHISLO:=CHISLO + 1;
расценивается семантическим анализатором как семантически корректная.
Закодированная цепочка лексем преобразуется в некоторое промежуточное представление, принятое на том или ином компьютере, например, в программу на языке ассемблера (для простоты используем некоторый условный ассемблер).
Создание промежуточного кода
Генерация промежуточного кода программы при компиляции
Полученный код избыточен: в самом деле, выполняется бессмысленная пересылка константы 1 и значения переменной CHISLO сначала в дополнительные переменные $1 и $2, а затем уже в регистры АХ и СХ, которые и используются при сложении. Аналогичные бессмысленные действия выполняются и с результатом сложения. Однако уменьшение размеров кода осуществляется на следующем этапе.
Промежуточный код может выглядеть по-разному. Он может связываться с реализуемым языком, например Р-код для языка Pascal, Diana для языка Ada, байт-код для языка Java. В качестве альтернативы он может также связываться с машинами, на которых осуществляется реализация. Пример: язык CTL (Compiler Target Language), который применялся в 70-х годах в Манчестерском университете в качестве промежуточного языка для машины MU5.
Промежуточный язык может быть близким к реализуемым языкам или к машинам, на которых осуществляется реализация. В любом случае он представляет собой линеаризацию синтаксического дерева, созданного в процессе синтаксического и семантического анализа, и формируется посредством разбиения древовидной структур на последовательность инструкций, каждая из которых эквивалентна одной или нескольким (небольшому числу) машинным командам. Машинный код может генерироваться, исходя только из промежуточного кода, хотя при этом может потребоваться доступ к таблице символов или другая информация, относящаяся к процессу компиляции.
Примеры промежуточного кода
Трехадресный код
Р - код
Байт-код
Здесь t — создаваемые компилятором временные имена. Можно также применять унарные операторы (monadic operator).
Например, оператор
-m
создаст инструкцию
t1 = -m
(хотя, безусловно, в этом случае трехадресный код будет содержать всего лишь два адреса!).
Преобразование выражений в последовательность инструкций трехадресного кода легко осуществляется с помощью анализатора на основе YACC.
Каждый оператор трехадресного кода имеет максимум три адреса, также существуют формы инструкций для присваивания, включающего адреса и указатели, вызовы процедур, вычисление параметров и т.д.
Высокоуровневые управляющие структуры, такие как циклы, условные операторы и операторы выбора для создания трехадресного кода, сводятся к проверкам условий и переходам. Приведем примеры того, как управляющие структуры компилируются в трехадресный код.
Приведенный выше оператор if можно реализовать путем добавления к грамматике следующих действий.
Здесь действиями являются: (см. далее)
Здесь действиями являются: (см. далее)
Каждая инструкция Р-кода имеет следующий формат.
F P Q
Здесь F — код функции, а Р или (иногда — и) Q могут отсутствовать в зависимости от конкретного кода. При наличии этих параметров Р может применяться для определения уровня статического блока, а Q — для определения офсета внутри фрейма или промежуточного операнда (например, константы). Инструкции без параметров применяются к верхним элементам стека и включают следующие инструкции:
and применяет булев оператор AND к верхним двум элементам стека, удаляет их и оставляет результат действия оператора (истина или ложь) на вершине стека.
dif применяет оператор разности множеств к верхним двум элементам стека, удаляет их и оставляет результат действия оператора (множество) на вершине стека,
ngi изменяет знак целого значения на вершине стека.
flt преобразует значение на вершине стека из целого в действительное.
flo преобразует значение второго сверху элемента стека из целого в действительное.
inn проверяет на предмет принадлежности к множеству, используя верхние два элемента стека как параметры и оставляя вместо них значения "истина" или "ложь'.
Здесь адреса времени компиляции представлены в виде пары целых чисел (статический уровень, офсет) Р-код также включает в себя инструкции переходов, например:
Метки могут устанавливаться в коде, например:
Единичные инструкции определяются таким образом, чтобы к значению на вершине стека можно было применить стандартные функции, например:
CSP ATAN
Здесь функция arctg применяется к значению на вершине стека, оставляя результат действия функции на месте этого значения. Другой пример:
CSP WLN
Здесь к файлу, который определяется верхним элементом стека, применяется инструкция writeln.
UJP L1
L2
При каждом применимом вхождении переменной образуется код для помещения в стек адреса или значения переменной (в зависимости от обстоятельств). Например,
LDA 1 7 будет загружать адрес (1, 7) на вершину стека, а
LODI 1 7 будет загружать значение целой переменной с адресом (1,7) на вершину стека.
Реализация присваивания, в простейшем случае, включает в себя копирование значения верхнего элемента стека в адрес второго сверху элемента стека. После этого два верхние элемента стека удаляются. Это можно сделать с помощью простой адресной инструкции.
Для каждого класса инструкции байт-кода находятся в классификационном файле Java (Java class file). В каждом файле содержится виртуальный машинный код для используемых классом методов (функций/процедур), информация таблицы символов (набор констант в Java), соединений с суперклассами и т.д. Для эффективной работы файл имеет двоичный формат, но для удобства просмотра его можно преобразовать в символьную форму.
Важной особенностью реализаций Java является то, что верификация происходит до выполнения программы, что позволяет избавиться от потенциально трудоемкой проверки в процессе выполнения. В то же время верификация обходится недешево: она основывается на разновидности средства доказательства теорем, имеющего некоторые теоретические ограничения.
Существует более 160 различных инструкций байт-кода, многие из них отличаются только типами операндов. Для верификации важным является хранение информации о типах в байт-коде, множество имеющихся инструкций по-разному поддерживают различные типы данных! Основные типы инструкций байт-кода можно выделить в такие группы:
работа со стеками;
выполнение арифметических операций;
оперирование объектами и массивами;
поток управления;
вызов методов;
обработка исключительных ситуаций и параллельная работа.
Инструкция Значение
incost_4 загружает в стек целую константу 4
inload_4 загружает в стек значение локальной переменной номер 4
pop отбрасывает верхнее значение стека
dup копирует верхний элемент стека
swap меняет местами два верхних элемента стека
istore_4 заносит значение верхнего элемента стека в локальную переменную номер 4
Примеры инструкций для выполнения арифметических операций.
Инструкция Значение
iadd суммирует две целых величины на вершине стека
fadd суммирует две величины типа float на вершине стека
fmul умножает две величины типа float на вершине стека
Доступ к массивам осуществляется с помощью инструкций, подобных приведенной ниже.
Инструкция Значение
iaload помещает значение элемента массива на вершину стека (предполагается, что ссылка массива и индекс индекса уже находятся в стеке)
Инструкция Значение
ifeq L1 переход к L1, если целое значение на вершине стека равно нулю
if_icmpne L1 переход к L1, если два целых значения на вершине стека различны
goto L1, переход к L1
Выше названы лишь несколько из богатого набора инструкций, предлагаемых JVM. JVM может использоваться (и в некоторой степени пользуется) как промежуточный этап для языков компиляции, отличных от Java (например, Ada).
Приведем байт-код, генерируемый для рассмотренных ранее в это разделе управляющих структур языка С.
1. if (выражение) оператор1 else оператор2
Данный оператор в байт-коде представляется в следующем виде.
байт-код для помещения значения выражения на вершину стека
ifeq L1
байт-код для выполнения оператора1
goto L2
L1
байт-код для выполнения оператора2
L2
Следует отметить, что в байт-коде значению "ложь" соответствует 0, а значению "истина" — 1. Отсюда — использование ifeq, а не обратной инструкции ifne. В языке Java булевский тип отсутствует.
В подходе, принятом Sun по отношению к реализации Java, были представлены некоторые интересные и существенно новые идеи для реализации языка. Подобно самому языку, методы его реализации продолжают дорабатываться и улучшаться.
Некоторые оптимизации тривиальны, другие требуют достаточно сложного анализа программы.
Наиболее распространенные оптимизации:
константные вычисления
уменьшение силы операций
выделение общих подвыражений
чистка циклов и т.д.
Из программы, полученной на предыдущем шаге, устраняются «лишние» операторы, переменные и константы, использование которых не влияет на корректность выполняемых действий
Пример 6:
Оптимизация промежуточного кода для программы из примера 5 приводит ее к виду:
CHISLO DD ?
MOVE 1, AX
MOVE CHISLO, CX
ADD AX, CX
MOVE AX, CHISLO
Такой результат связан с тем, что ряд операторов выполняли лишние перемещения данных из одного места хранения в другое. Удаление этих операторов повлекло удаление вспомогательных переменных.
Алгоритм устроен следующим образом:
Цикл по всем дугам графа управления
Если вероятность дуги равна нулю
Добавить все узлы (кроме STOP),достижимые вниз из дуги, в множество С копируемых узлов
Пометить дугу маркером
Конец если
Конец цикла
Цикл по всем узлам из множества С
Создать копию узла
Пометить копию специальной меткой
Цикл по всем дугам, входящим в узел
Если дуга помечена маркером
Перенаправить дугу на копию
Конец цикла
Конец цикла
Цикл по всем узлам графа управления
Если узел недостижим из узла START
Удалить узел
Конец цикла
Таким образом обеспечивается корректная структура последователей. Перенаправление дуг необходимо для обеспечения наличия пути из узла START в начальный узел копируемого подграфа. Последний цикл алгоритма необходим для удаления излишних, недостижимых из узла START узлов, появившихся после перенаправления дуг. Тем самым обеспечивается связность и корректная структура графа управления в целом.
В результате работы алгоритма получаем модифицированный граф, представленный на Рис. 4.
Копии узлов, получаемые в результате работы алгоритма, помечаются специальным признаком, который обозначает нулевую вероятность передачи управления в этот узел. Будем называть эти узлы, помеченные этим признаком, узлами «черной дыры».
Использование результатов оптимизации при компиляции
Рассмотрим исходную ситуацию, представленную на Рис. 2. Как видно из рисунка, управление по дуге со значением вероятности 1.0 и управление по дуге со значением вероятности 0.0 сходятся в узле Node 7. Различные оптимизации, такие как if-conversion, глобальное планирование, основываясь на профильной информации, которая содержится в узлах и дугах, охватывающих представленный подграф, могут преобразовать весь подграф в единый узел, называемый расширенным скалярным участком , перемешав, таким образом, вычисления, находящиеся на ветках управления с разными вероятностями исполнения.
В результате применения алгоритма получается новый подграф, в котором эти ветки управления не пересекаются. Узлы, помеченные признаком «черной дыры», обрабатываются оптимизациями специальными способами. В частности, if-conversion и глобальное планирование не преобразует такие узлы в расширенные скалярные участки. Отказываясь, таким образом, от применения оптимизаций в области «черной дыры», мы тем самым сохраняем чистоту высоковероятных вычислений.
Подобным образом могут обрабатывать такие узлы и остальные оптимизации. Таким образом, получается, что область «черной дыры» существует отдельно от основной части программы, и вычисления, находящиеся в ней, не попадают в код с большим количеством исполнений.
Иногда полезно модифицировать алгоритм и требовать равенства значения вероятности дуги не нулю, а некоторому малому числу в окрестности нуля. Подобрав этот параметр, можно улучшить производительность полученного кода. Такой эксперимент был поставлен на задаче 147.vortex из пакета Spec95.
Объектная программа может быть
последовательностью абсолютных машинных команд
последовательностью перемещаемых машинных команд
программой на языке ассемблера
программой на некотором другом языке
Наиболее эффективным методом с точки зрения скорости компиляции является отображение исходной программы в абсолютную программу на машинном языке, пригодную для непосредственного исполнения. Такой тип компиляции больше всего подходит для небольших программ, не использующих независимо компилируемых подпрограмм.
Однако для более сложных программ необходимо создавать объектные программы в форме последовательности перемещаемых машинных команд. Перемещаемая машинная команда представляет собой команду, в которой адресация ячеек памяти производится относительно некоторого подвижного начала. Объектную программу называют также перемещаемым объектным сегментом. Этот сегмент может быть связан с другими сегментами, такими, как независимо компилируемые подпрограммы пользователя, подпрограммы ввода-вывода и библиотечные функции.
Такое связывание (создание единого перемещаемого объектного сегмента из набора различных сегментов) осуществляется программой, которая называется редактором связей. Далее единый перемещаемый объектный сегмент или модуль загрузки размещается в памяти программой, называемой загрузчиком, которая превращает перемещаемые адреса в абсолютные. После этого программа готова к выполнению.
Оператор DD выделяет объем памяти в 2 байта для описываемой переменной. Тогда схема размещения объектного кода имеет вид, представленный в таблице:
Помимо собственно генерации кода, на этом этапе необходимо решить множество сопутствующих проблем, например:
распределение памяти, т.е. отображение имен исходной программы в адреса памяти
распределение регистров, т.е. определение для каждой точки программы множества переменных, которые должны быть размещены в регистрах
выбор такой последовательности записи значений в регистры, которая избавила бы от необходимости частой выгрузки значений из регистров, а затем повторной загрузки
Трансляция в ассемблер
Виртуальная машина
Способы получения объектного кода
Кросс-транслятор
Например, пусть для выполнения программы из таблицы раздела Трансляция отводится область основной памяти, начиная с абсолютного адреса 40. Тогда, с учетом сегментированной схемы адресации указанные в таблице адреса ( с учетом абсолютного адреса 40) преобразуются в свои сегментированные эквиваленты:
В полученных сегментированных адресах номера сегментов хранятся в специальных регистрах процессора и в коды команд не включаются, а включаются лишь смещения. Тогда объектный код из таблицы преобразуется в следующий вид:
Тестирование программы имеет целью определение работоспособности программы на всем требуемом диапазоне исходных данных. Программистом составляется представительная выборка исходной информации, которая позволит определить корректность программы при любых входных параметрах.
Для программы из примера 1 задаются набором переменной CHISLO. Так, если среди этих значений будет такое, которое превышает допустимый диапазон типа integer, при выполнении программы в процессе тестирования возникает переполнение разрядной сетки (overflow), что обсуждалось ранее. Это потребует от программиста переделки исходной программы в части описания переменной CHISLO.
Пример 7:
Для программы
var CHISLO: integer;
CHISLO:=CHISLO + 1;
и (ее кода) будут выявлены две логические ошибки:
неизвестность результата ввиду отсутствия в программе оператора вывода, например, write (CHISLO);
после добавления оператора вывода в программу и выполнения последней, полученное потребителем значение переменной CHISLO имеет произвольный характер, так как не определено начальное значение этой переменной.
Тогда правильная программа вместо той, которая приведена в примере 1, могла бы иметь вид:
var CHISLO: integer;
input (CHISLO);
CHISLO:=CHISLO+1;
write (CHISLO);
Понятно, что архитектура виртуальной машины должна быть разработана таким образом, чтобы конструкции исходного языка удобно отображались в систему команд и сама система команд не была слишком сложной. При выполнении этих условий можно достаточно быстро написать интерпретатор виртуальной машины.
Одна из первых широко известных виртуальных машин была разработана в 70-х годах Н. Виртом при написании компилятора Pascal-P. Этот компилятор генерировал специальный код, названный P-кодом и представляющий собой последовательность инструкций гипотетической стековой машины. Сегодня идея виртуальных машин приобрела широкую известность благодаря языку Java, компиляторы которого обычно генерируют так называемый байт-код , т.е. объектный код, который представляет собой последовательность команд виртуальной Java-машины.
Компиляция на лету
Виртуальная JAVA машина
Внешний и внутренний интерфейсы
Просмотры
Техника «заплат»
Часто JIT-компилятор используется вместе с интерпретатором виртуальной машины. Организовывается это следующим образом. Вначале сгенерированный байт-код поступает на вход интерпретатору виртуальной машины, которая его интерпретирует. Одновременно с интерпретатором работает программа, которая вычисляет время интерпретации какого-то куска байт-кода, например, процедуры. Если оказывается, что время интерпретации некоторого куска кода достаточно большое, то вызывается JIT-компилятор, который транслирует его в "родные" машинные коды. Когда при выполнении приложения произойдет повторное обращение к этому куску кода, то он уже не будет интерпретироваться, а будет выполняться сгенерированный фрагмент машинного кода.
Использование связки "компилятор+интерпретатор+JIT-компилятор" позволяет заметно повысить скорость выполнения исходной программы, причем переносимость кода, создаваемого компилятором, естественно, сохраняется.
Все регистры 32-разрядные. Стек-фрейм имеет три компоненты: локальные переменные, среду исполнения, стек операндов. Локальные переменные отсчитываются от адреса в регистре vars. Среда исполнения служит для поддержания самого стека. Она включает указатель на предыдущий фрейм, указатель на собственные локальные переменные, на базу стека операций и на верхушку стека. Кроме того, здесь же хранится некоторая дополнительная информация, например, для отладчика.
Куча сборки мусора содержит экземпляры объектов, которые создаются и уничтожаются автоматически. Область методов содержит коды, таблицы символов и т.д.
С каждым классом связана область констант. Она содержит имена полей, методов и другую подобную информацию, которая используется методами.
Помещение локальных переменных в стек
Вызов метода
Обработка исключительных ситуаций
Команда isore – сохранить целое в локальной переменной. Операндом операции является смещение переменной в области локальных переменных. Значение с верхушки стека операций копируется в указываемую область локальных переменных. Имеются аналогичные команды для помещения плавающих, двойных целых, двойных плавающих и т.д.
Одна или несколько фаз компиляции могут выполняться на одном просмотре. Например, лексический анализ и синтаксический анализ часто выполняются на одном просмотре, т.е. синтаксический анализатор может обращаться к лексическому анализатору за очередной лексемой лишь по мере необходимости. С другой стороны, некоторые оптимизации могут выполняться на нескольких просмотрах.
Передача информации между просмотрами происходит в терминах так называемых промежуточных языков. Таким образом, если компилятор состоит из N просмотров, то должно быть определено N-1 промежуточных языков. Каков этот промежуточный язык, зависит от разработчиков компилятора. Обычно программа на промежуточном языке представляет собой синтаксическое дерево и, возможно, какие-то внутренние таблицы компилятора.
Желательно иметь относительно мало просмотров, т.к. для чтения программы на одном промежуточном языке и записи ее на другом промежуточном языке может занимать довольно много времени. С другой стороны, объединяя несколько фаз в один просмотр, мы должны помнить о том, что иногда мы не можем выполнить некоторую фазу, не получив информацию из предыдущих фаз. Например, C# позволяет использовать имя метода до того, как он был описан. Понятно, что мы не можем выполнить видозависимый анализ до тех пор, пока мы не будем знать имена и типы всех методов объектов. Таким образом, эти задачи должны решаться на разных просмотрах.
Транслятор может при генерации кода сгенерировать вначале скелет команды перехода, запомнив ее адрес в строке некоторой таблицы, соответствующей идентификатору target. Когда адрес этого идентификатора будет определен, мы сможем завершить формирование команд перехода, пробежав по имеющемуся у нас списку.
Если не удалось найти и скачать презентацию, Вы можете заказать его на нашем сайте. Мы постараемся найти нужный Вам материал и отправим по электронной почте. Не стесняйтесь обращаться к нам, если у вас возникли вопросы или пожелания:
Email: Нажмите что бы посмотреть