3.2. Компилятор
Язык программирования без компилятора (или интерпретатора) может представлять большой теоретический интерес, но выполнить на компьютере программу, написанную на этом языке, невозможно. Связь между языками и компиляторами настолько тесная, что различие между ними расплывается, и часто можно услышать такую бессмыслицу, как:
Язык L1 эффективнее языка L2.
Правильно же то, что компилятор С1 может сгенерировать более эффективный код, чем компилятор С2, или что легче эффективно откомпилировать конструкции L1, чем соответствующие конструкции L2. Одна из целей этой книги — показать соотношение между конструкциями языка и получающимся после компиляции машинным кодом.
Структура компилятора показана на рис. 3.1. Входная часть компилятора
«понимает» программу, анализируя синтаксис и семантику согласно правилам языка. Синтаксический анализатор отвечает за преобразование последовательности символов в абстрактные синтаксические объекты, называемые лексемами. Например, символ «=» в языке С преобразуется в оператор присваивания, если за ним не следует другой «=»; в противном случае оба соседних символа «=» (т.е. «==») преобразуются в операцию проверки равенства. Анализатор семантики отвечает за придание смысла этим абстрактным объектам. Например, в следующей программе семантический анализатор выделит глобальный адрес для первого i и вычислит смещение параметра — для второго i:
с |
void proc(inti) {... }
Результат работы входной части компилятора — абстрактное представление программы, которое называется промежуточным представлением. По нему можно восстановить исходный текст программы, за исключением имен идентификаторов и физического формата строк, пробелов, комментариев и т.д.
Исследования в области компиляторов настолько продвинуты, что входная часть может быть автоматически сгенерирована по грамматике (формальному описанию) языка. Читателям, интересующимся разработкой языков программирования, настоятельно рекомендуется глубоко изучить компиляцию и разрабатывать языки так, чтобы их было легко компилировать с помощью автоматизированных методов.
Выходная часть компилятора берет промежуточное представление программы и генерирует машинный код для конкретного компьютера. Таким образом, входная часть является языковозависимой, в то время как выходная — машиннозависимой. Поставщик компиляторов может получить семейство компиляторов некоторого языка L для ряда самых разных компьютеров Cl, C2,..., написав несколько выходных частей, использующих промежуточное представление общей входной части. Точно так же поставщик компьютеров может создать высококачественную выходную часть для компьютера С и затем поддерживать большое число языков LI, L2,..., написав входные части, которые компилируют исходный текст каждого языка в общее промежуточное представление. В этом случае фактически не имеет смысла спрашивать, какой язык на компьютере эффективнее.
С генератором объектного кода связан оптимизатор, который пытается улучшать код, чтобы сделать его более эффективным. Возможны несколько способов оптимизации:
• Оптимизация промежуточного представления, например нахождение общего подвыражения:
a = f1 (x + y) + f2(x + y);
Вместо того чтобы вычислять выражение х + у дважды, его можно вычислить один раз и сохранить во временной переменной или регистре. Подобная оптимизация не зависит от конкретного компьютера и может быть сделана до генерации кода. Это означает, что даже компоненты выходной части могут быть общими в компиляторах разных компьютеров.
• Машинно-ориентированная оптимизация. Такая оптимизация, как сохранение промежуточных результатов в регистрах, а не в памяти, явно должна выполняться при генерации объектного кода, потому что число и тип регистров в разных компьютерах различны.
• Локальная оптимизация обычно выполняется для сгенерированных команд, хотя иногда ее можно проводить для промежуточного представления. В этой методике делается попытка заменять короткие последовательности команд одной, более эффективной командой. Например, в языке С выражение n++ может быть скомпилировано в следующую последовательность:
load R1,n
add R1,#1
store R1,n
но локальный оптимизатор для конкретного компьютера мог бы заменить эти три команды одной, которая увеличивает на единицу непосредственно слово в памяти:
incr n
Использование оптимизаторов требует осторожности. Поскольку оптимизатор по определению изменяет программу, ее, возможно, будет трудно отлаживать с помощью отладчика исходного кода, так как порядок выполнения команд может отличаться от их порядка в исходном коде. Обычно оптимизатор при отладке лучше отключать. Кроме того, из-за сложности оптимизатора вероятность содержания в нем ошибки больше, чем в любом другом компоненте компилятора. Ошибку оптимизатора трудно обнаружить, потому что отладчик создан для работы с исходным текстом, а не с оптимизированным (то есть измененным) объектным кодом. Ни в коем случае нельзя сначала тестировать программу без оптимизатора, а после оптимизации отдавать в работу без тестирования. Наконец, оптимизатор в какой-либо ситуации может сделать неправильные предположения. Например, для устройства ввода-вывода с регистрами, «отображенными» на память, значение переменной может присваиваться дважды без промежуточного чтения:
с |
transmit_register = 0x70; /* Ждать 1 секунду */ transmit_register = 0x70;
Оптимизатор предположит, что второе присваивание лишнее и удалит его из сгенерированного объектного кода.
- Глава 1
- 1.2. Процедурные языки
- 1.3. Языки, ориентированные на данные
- 1.4. Объектно-ориентированные языки
- 1.5. Непроцедурные языки
- 1.6. Стандартизация
- 1.7. Архитектура компьютера
- 1.8. Вычислимость
- 1.9. Упражнения
- Глава 2
- 2.2. Семантика
- 2.3. Данные
- 2.4. Оператор присваивания
- 2.5. Контроль соответствия типов
- 2.7. Подпрограммы
- 2.8. Модули
- 2.9. Упражнения
- Глава 3
- 3.1. Редактор
- 3.2. Компилятор
- 3.3. Библиотекарь
- 3.4. Компоновщик
- 3.5. Загрузчик
- 3.6. Отладчик
- 3.7. Профилировщик
- 3.8. Средства тестирования
- 3.9. Средства конфигурирования
- 3.10. Интерпретаторы
- 3.11. Упражнения
- Глава 4
- 4.1. Целочисленные типы
- I: Integer; -- Целое со знаком в языке Ada
- 4.2. Типы перечисления
- 4.3. Символьный тип
- 4.4. Булев тип
- 4.5. Подтипы
- 4.6. Производные типы
- 4.7. Выражения
- 4.8. Операторы присваивания
- 4.9. Упражнения
- Глава 5
- 5.1. Записи
- 5.2. Массивы
- 5.3. Массивы и контроль соответствия типов
- Подтипы массивов в языке Ada
- 5.5. Строковый тип
- 5.6. Многомерные массивы
- 5.7. Реализация массивов
- 5.8. Спецификация представления
- 5.9. Упражнения
- Глава 6
- 6.1. Операторы switch и case
- 6.2. Условные операторы
- 6.3. Операторы цикла
- 6.4. Цикл for
- 6.5. «Часовые»
- 6.6. Инварианты
- 6.7. Операторы goto
- 6.8. Упражнения
- Глава 7
- 7.1. Подпрограммы: процедуры и функции
- 7.2. Параметры
- 7.3. Передача параметров подпрограмме
- 7.4. Блочная структура
- 7.5. Рекурсия
- 7.6. Стековая архитектура
- 7.7. Еще о стековой архитектуре
- 7.8. Реализация на процессоре Intel 8086
- 7.9. Упражнения
- Глава 8
- 8.1 . Указательные типы
- 8.2. Структуры данных
- 8.3. Распределение памяти
- 8.4. Алгоритмы распределения динамической памяти
- 8.5. Упражнения
- Глава 9
- 9.1. Представление вещественных чисел
- 9.2. Языковая поддержка вещественных чисел
- 9.3. Три смертных греха
- Вещественные типы в языке Ada
- 9.5. Упражнения
- Глава 10
- 10.1. Преобразование типов
- 10.2. Перегрузка
- 10.3. Родовые (настраиваемые) сегменты
- 10.4. Вариантные записи
- 10.5. Динамическая диспетчеризация
- 10.6. Упражнения
- Глава 11
- 11.1. Требования обработки исключительных ситуаций
- 11.2. Исключения в pl/I
- 11.3. Исключения в Ada
- 11.5. Обработка ошибок в языке Eiffei
- 11.6. Упражнения
- Глава 12
- 12.1. Что такое параллелизм?
- 12.2. Общая память
- 12.3. Проблема взаимных исключений
- 12.4. Мониторы и защищенные переменные
- 12.5. Передача сообщений
- 12.6. Язык параллельного программирования оссаm
- 12.7. Рандеву в языке Ada
- 12.9. Упражнения
- Глава 13
- 13.1. Раздельная компиляция
- 13.2. Почему необходимы модули?
- 13.3. Пакеты в языке Ada
- 13.4. Абстрактные типы данных в языке Ada
- 13.6. Упражнения
- Глава 14
- 14.1. Объектно-ориентированное проектирование
- В каждом объекте должно скрываться одно важное проектное решение.
- 14.3. Наследование
- 14.5. Объектно-ориентированное программирование на языке Ada 95
- Динамический полиморфизм в языке Ada 95 имеет место, когда фактический параметр относится к cw-типу, а формальный параметр относится к конкретному типу.
- 14.6. Упражнения
- Глава 15
- 1. Структурированные классы.
- 15.1. Структурированные классы
- 5.2. Доступ к приватным компонентам
- 15.3. Данные класса
- 15.4. Язык программирования Eiffel
- Если свойство унаследовано от класса предка более чем одним путем, оно используется совместно; в противном случае свойства реплицируются.
- 15.5. Проектные соображения
- 15.6. Методы динамического полиморфизма
- 15.7. Упражнения
- 5Непроцедурные
- Глава 16
- 16.1. Почему именно функциональное программирование?
- 16.2. Функции
- 16.3. Составные типы
- 16.4. Функции более высокого порядка
- 16.5. Ленивые и жадные вычисления
- 16.6. Исключения
- 16.7. Среда
- 16.8. Упражнения
- Глава 17
- 17.2. Унификация
- 17.4. Более сложные понятия логического программирования
- 17.5. Упражнения
- Глава 18
- 18.1. Модель Java
- 18.2. Язык Java
- 18.3. Семантика ссылки
- 18.4. Полиморфные структуры данных
- 18.5. Инкапсуляция
- 18.6. Параллелизм
- 18.7. Библиотеки Java
- 8.8. Упражнения