Генерация высококачественного кода для программ на СИ - Методы оптимизации. Часть 2
ОГЛАВЛЕНИЕ
Страница 4 из 17
"Снижение мощности" подразумевает замещение операций, которые требуют
большего времени выполнения, более быстрыми. Компилятор может применять
снижение мощности несколькими способами. Например, применяя снижение
мощности к сгенерированному коду, компилятор может подменять операции,
которые умножают или делят целые числа на степени двойки, операциями
сдвига.
"Удаление недостижимого кода" - еще один метод оптимизации. Недостижимый
код - это некоторая последовательность инструкций программы, которая
недостижима ни по одному пути в программе. Он может образоваться как
следствие предыдущих операций оптимизации, кода условной отладки, или
частых изменений программы многими программистами. Следующие операторы -
это вариант кода для проверки компилятора на выполнение этого метода
оптимизации.
#define DEBUG 0
if(DEBUG)
printf("Debug Function\n");
Манифестные константы часто могут скрывать существование недостижимого
кода, особенно если такой код определяется внутри включаемого
файла-заголовка.
"Удаление лишних присваиваний" включает нахождение промежутка жизни
переменной и удаление присваиваний этой переменной, если эти присваивания
не могут изменить логику программы. Этот метод освобождает ограниченные
ресурсы, такие как пространство стека или машинные регистры. В следующей
последовательности команд:
a = 5;
b = 0;
a = b;
первый оператор есть лишнее присваивание и может быть удален безопасно.
Лишние присваивания могут возникать непреднамеренно, когда промежуток
жизни переменной велик и между вхождениями переменной имеется более-менее
длинный код. Лишние присваивания могут быть также результатом предыдущих
проходов оптимизации.
Цель "распределения переменных по регистрам" состоит в попытке обеспечить
оптимальное назначение регистров путем сохранения часто используемых
переменных в регистрах так долго, как это возможно, для того, чтобы
исключить более медленный доступ к памяти. Количество регистров, доступных
для использования, зависит от архитектуры процессора. Семейство
микропроцессоров Intel 80x86 резервирует много регистров для специального
использования и имеет несколько универсальных регистров. В помощь
распределению переменных по регистрам язык Си предоставляет спецификатор
класса регистровой памяти, который дает возможность программисту
указывать, какие переменные должны располагаться в регистрах.
При назначении переменных регистрам компилятор принимает во внимание не
только какие переменные нужно выделить, но также и регистры, которым они
назначаются. Выбор переменных зависит от частоты их использования,
промежутков жизни текущих регистровых переменных (которые определяются при
анализе потоков данных) и количества доступных регистров. В зависимости от
степени выполняемой компилятором оптимизации промежуток жизни переменной
может определяться внутри единственного оператора, внутри базового блока
или перекрывать несколько базовых блоков. Переменная сохраняется в
регистре только если она будет снова использоваться. Если на переменную в
дальнейшем не будет ссылок, то она сохраняется в оперативной памяти,
освобождая регистр для другой переменной.
Поскольку оптимизирующему компилятору известен промежуток жизни
переменной, он не будет намеренно генерировать "лишние операции сохранения
и загрузки" (регистров). Лишние операции сохранения удаляются посредством
удаления излишних присваиваний; лишние операции загрузки опускаются с
помощью усовершенствованного распределения переменных по регистрам. Имея
текст:
a = i + 2;
b = a + 3;
компилятор без возможностей оптимизации может сгенерировать следующий код:
mov AX,i
add AX,2
mov a,AX
mov AX,a
add AX,3
mov b,AX
тогда как оптимизирующий компилятор может использовать механизм размещения
переменных в регистрах для удаления лишней четвертой инструкции (mov
AX,a).