- •Глава 14 Генерация
- •1 Примером контекстной зависимости, часто встречающейся во многих языках программирования, может служить необходимость предварительно описать идентификатор до его первого использования.
- •Этапы семантического анализа
- •Идентификация лексических единиц языков программирования
- •Распределение памяти. Принципы распределения памяти
- •1 Некоторые вычислительные системы даже вообще не могут работать с адресами, не кратными определенному числу байт.
- •Глобальная и локальная память
- •Статическая и динамическая память
- •Дисплей памяти процедуры (функции). Стековая организация дисплея памяти
- •Стековая организация дисплея памяти процедуры (функции)
- •Память для типов данных (rtti-информация)
- •Способы внутреннего представления программ
- •Синтаксические деревья
- •Многоадресный код с явно именуемым результатом (тетрады)
- •Многоадресный код с неявно именуемым результатом (триады)
- •Обратная польская запись операций
- •Вычисление выражений с помощью обратной польской записи
- •Схемы су-перевода
- •Пример схемы су-перевода дерева операций на язык ассемблера
- •Пример схемы су-перевода дерева операций в последовательность триад
- •Оптимизация линейных участков программы Принципы оптимизации линейных участков
- •Свертка объектного кода
- •Исключение лишних операций
- •Другие методы оптимизации программ Оптимизация вычисления логических выражений
- •Оптимизация передачи параметров в процедуры и функции
- •Оптимизация циклов
- •1 Незначительный выигрыш в скорости достигается за счет исключения всех операций с индуктивной переменной, однако объем программы может существенно возрасти.
- •Машинно-зависимые методы оптимизации
- •Распределение регистров процессора
- •Оптимизация кода для процессоров, допускающих распараллеливание вычислений
1 Незначительный выигрыш в скорости достигается за счет исключения всех операций с индуктивной переменной, однако объем программы может существенно возрасти.
Машинно-зависимые методы оптимизации
Машинно-зависимые методы оптимизации ориентированы на конкретную архитектуру целевой вычислительной системы, на которой будет выполняться результирующая программа. Как правило, каждый компилятор ориентирован на I одну определенную архитектуру целевой вычислительной системы. Иногда возможно в настройках компилятора явно указать одну из допустимых целевых архитектур. B любом случае результирующая программа всегда порождается для четко заданной целевой архитектуры.
Архитектура вычислительной системы есть представление аппаратной и программной составляющих частей системы и взаимосвязи между ними с точки зрения системы как единого целого. Понятие «архитектура» включает в себя особенности и аппаратных, и программных средств целевой вычислительной системы. При выполнении машинно-зависимой оптимизации компилятор может принимать во внимание те или иные ее составляющие. To, какие конкретно особенности архитектуры будут приняты во внимание, зависит от реализации компилятора и определяется его разработчиками.
Количество существующих архитектур вычислительных систем к настоящему времени очень велико. Поэтому не представляется возможным рассмотреть все ориентированные на них методы оптимизации даже в форме краткого обзора. Интересующиеся этим вопросом могут обратиться к специализированной литературе [6, т. 2, 40, 74, 82]. Далее будут рассмотрены только основные два аспекта машинно-зависимой оптимизации: распределение регистров процессора и порождение кода для параллельных вычислений.
Распределение регистров процессора
Процессоры, на базе которых строятся современные вычислительные системы, имеют, как правило, несколько программно-доступных регистров. Часть из них может быть предназначена для выполнения каких-либо определенных целей (например, регистр — указатель стека или регистр — счетчик команд), другие могут быть использованы практически произвольным образом при выполнении различных операций (так называемые «регистры общего назначения»).
Использование регистров общего назначения для хранения значений операндов и результатов вычислений позволяет добиться увеличения быстродействия программы, так как действия над регистрами процессора всегда выполняются быстрее, чем над ячейками памяти. Кроме того, в ряде процессоров не все операции могут быть выполнены над ячейками памяти, а потому часто требуется предварительная загрузка операнда в регистр. Результат выполнения операции чаще всего тоже оказывается в регистре, и если необходимо, его надо выгрузить (записать) в ячейку памяти.
Программно доступных регистров процессора всегда ограниченное количество. I Поэтому встает вопрос об их распределении при выполнении вычислений. Этим занимается алгоритм распределения регистров, который присутствует практически в каждом современном компиляторе в части генерации кода результирующей программы.
При распределении регистров под хранение промежуточных и окончательных результатов вычислений может возникнуть ситуация, когда значение той или иной переменной (связанной с нею ячейки памяти) необходимо загрузить в регистр для дальнейших вычислений, а все имеющиеся доступные регистры уже заняты. Тогда компилятору перед созданием кода по загрузке переменной в регистр необходимо сгенерировать код для выгрузки одного из значений из регистра в ячейку памяти (чтобы освободить регистр). Причем выгружаемое значение затем, возможно, придется снова загружать в регистр. Встает вопрос о выборе того регистра значение которого нужно выгрузить в память.
При этом возникает необходимость выработки стратегии замещения регистров процессора. Она чем-то сродни стратегиям замещения процессов и страниц в памяти компьютера, которые были рассмотрены в части 1 данного пособия. Однако отличие заключается в том, что, в отличие от диспетчера памяти в OC, компилятор может проанализировать код и выяснить, какое из выгружаемых значений ему понадобится для дальнейших вычислений и когда (следует помнить, что компилятор сам вычислений не производит — он только порождает код для них, иное дело — интерпретатор). Как правило, стратегии замещения регистров процессора предусматривают, что выгружается тот регистр, значение которого будет использовано в последующих операциях позже всех (хотя не всегда эта стратегия является оптимальной).
Кроме общего распределения регистров могут использоваться алгоритмы распределения регистров специального характера. Например, во многих процессорах есть регистр-аккумулятор, который ориентирован на выполнение различных арифметических операций (операции с ним выполняются либо быстрее, либо имеют более короткую запись). Поэтому в него стремятся всякий раз загрузить чаще всего используемые операнды; он же используется, как правило, при передаче результатов функций и отдельных операторов. Могут быть также регистры, ориентированные на хранение счетчиков циклов, базовых указателей и т. п. Тогда компилятор должен стремиться распределить их именно для тех целей, на выполнение которых они ориентированы.
