Дизайн компилятора - оптимизация кода

Оптимизация - это метод преобразования программы, который пытается улучшить код, заставляя его потреблять меньше ресурсов (т. Е. ЦП, память) и обеспечивать высокую скорость.

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

  • Выходной код никоим образом не должен изменять значение программы.

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

  • Оптимизация должна быть быстрой и не должна задерживать весь процесс компиляции.

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

  • В начале пользователи могут изменять / переставлять код или использовать лучшие алгоритмы для написания кода.

  • После генерации промежуточного кода компилятор может модифицировать промежуточный код путем вычисления адресов и улучшения циклов.

  • При создании целевого машинного кода компилятор может использовать иерархию памяти и регистры процессора.

Оптимизацию можно разделить на два типа: машинно-независимый и машинно-зависимый.

Машинно-независимая оптимизация

При этой оптимизации компилятор принимает промежуточный код и преобразует часть кода, которая не включает какие-либо регистры ЦП и / или абсолютные области памяти. Например:

do
{
   item = 10;
   value = value + item; 
} while(value<100);

Этот код включает в себя повторное присвоение идентификатора элемента, который, если мы скажем так:

Item = 10;
do
{
   value = value + item; 
} while(value<100);

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

Машинно-зависимая оптимизация

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

Основные блоки

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

Программа может иметь различные конструкции в качестве базовых блоков, таких как условные операторы IF-THEN-ELSE, SWITCH-CASE и циклы, такие как DO-WHILE, FOR и REPEAT-UNTIL и т. Д.

Идентификация основного блока

Мы можем использовать следующий алгоритм, чтобы найти основные блоки в программе:

  • Искать в заголовках всех базовых блоков, с которых начинается базовый блок:

    • Первое утверждение программы.
    • Заявления, которые являются целью любой ветви (условные / безусловные).
    • Заявления, которые следуют за любым заявлением ветви.
  • Операторы заголовка и операторы, следующие за ними, образуют основной блок.

  • Базовый блок не включает в себя какой-либо оператор заголовка любого другого базового блока.

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

Основные блоки

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

График потока управления

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

График потока управления

Loop Optimization

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

  • Инвариантный код : фрагмент кода, который находится в цикле и вычисляет одно и то же значение на каждой итерации, называется кодом, инвариантным к циклу. Этот код можно вывести из цикла, сохранив его для вычисления только один раз, а не с каждой итерацией.

  • Индукционный анализ : переменная называется индукционной переменной, если ее значение в цикле изменяется инвариантным значением цикла.

  • Снижение прочности : есть выражения, которые потребляют больше циклов ЦП, времени и памяти. Эти выражения должны быть заменены более дешевыми выражениями без ущерба для вывода выражения. Например, умножение (x * 2) дороже с точки зрения циклов ЦП, чем (x << 1) и дает тот же результат.

Устранение мертвого кода

Мертвый код - это один или несколько операторов кода, которые:

  • Либо никогда не выполняется, либо недоступен,
  • Или, если выполняется, их вывод никогда не используется.

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

Частично мертвый код

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

Частично мертвый код

На приведенном выше графике потока управления изображен фрагмент программы, в котором переменная «a» используется для назначения вывода выражения «x * y». Предположим, что значение, присвоенное 'a', никогда не используется внутри цикла. Сразу после того, как элемент управления покидает цикл, 'a' присваивается значение переменной 'z', которое будет использоваться позже в программе. Здесь мы заключаем, что код присваивания «а» нигде не используется, поэтому он может быть исключен.

Мертвый код

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

Частичное резервирование

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

Избыточное выражение

[избыточное выражение]

Частично избыточное выражение

[частично избыточное выражение]

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

Другим примером частично избыточного кода может быть:

If (condition)
{
   a = y OP z;
}
else
{
   ...
}
c = y OP z;

Мы предполагаем, что значения операндов ( y и z ) не изменяются от присвоения переменной a переменной c . Здесь, если выражение условия истинно, тогда y OP z вычисляется дважды, в противном случае - один раз. Кодовое движение может использоваться для устранения этой избыточности, как показано ниже:

If (condition)
{
   ...
   tmp = y OP z;
   a = tmp;
   ...
}
else
{
   ...
   tmp = y OP z;
}
c = tmp;

Здесь, является ли условие истинным или ложным; y OP z следует вычислять только один раз.