- Об оптимизаторе программ
- Работа
Об оптимизаторе программ
Рассмотренные средства языка Си позволяют строить программы произвольной сложности. Удобство языка высокого уровня может создать ложное впечатление о «вседозволенности» или «безграничных возможностях» языка, т.е. может показаться, что любая программа будет очень качественной лишь благодаря средствам Си, а от программиста не требуется повышенных усилий при ее разработке, как, например, это необходимо при работе на ассемблере.
Увы, использование языка высокого уровня вызывает неизбежное увеличение размера итогового программного кода, повышает требовательность программы к памяти и быстродействию микроконтроллера. Для систем на базе персональных компьютеров с сотнями мегабайт доступного ОЗУ и гигагерцами тактовой частоты процессора эти проблемы, возможно, и не особенно актуальны, но не для микроконтроллеров AVR, с их единицами килобайт ОЗУ (а то и десятками байт) и весьма скоромной производительностью ядра.
Чтобы хоть как-то снизить остроту этой проблемы, все компиляторы Си содержат средства встроенной оптимизации генерируемого кода. В чем же заключается оптимизация и как она осуществляется компилятором?
Обратимся к функции, рассмотренной в разделе «static-переменные». Если задуматься, то как бы эта функция не использовалась в программе, ее можно с успехом заменить обращением к переменной result с автоинкрементом (если бы эта переменная была не локальной, а глобальной), и тем самым исключить лишний код и расходы стековой памяти на реализацию обращений к функции. Но программист, предположим, не посчитал нужным так поступить, и тогда в дело вступает оптимизатор компилятора, который незаметно изменяет логику программы, не нарушая ее принципиально. То есть вместо обращения к функции func() компилятор встраивает код result++, переместив при этом переменную result из области локальных переменных к глобальным. Для программиста все остается, как было, но фактически программа уже немного не та...
Другой способ оптимизации заключается, например, в том, что компилятор может добиться более высокой скорости выполнения цикла, заменив его на следующие друг за другом участки одинаковой функциональности, т.е. фактически аннулируя смысл оператора цикла. «Развернутый» цикл может быть больше по размеру кода, но часто оказывается оптимальнее по требованиям к ОЗУ и более быстродействующим.
Практически всегда компилятор заменяет функцию, которая используется лишь однажды в программе на эквивалентные операторы, помещенные прямо в месте вызова функции. Так же у компилятора хватает интеллекта разобраться с неизменными значениями выражений и заменить несколько операторов сразу константой результата. Пример:
define MAX 15
int sum, i = 10;
while (i < MAX){
if (i == 9) break;
i++;
sum *= i;
}
Если проанализировать этот участок кода, то окажется, что несмотря на всю его сложность, значение переменной sum всегда будет равно нулю. Но это значение переменная и так получает по умолчанию при описании, следовательно, оператор цикла можно устранить из программы, никак не нарушив ее функционирования.
Этот пример, конечно, из разряда казусов. Однако такие казусы случаются нередко, приводя, порой, к занимательным результатам: если представить, что значение sum используется далее в программе, то автоматически могут быть исключены и другие участки кода:
if (sum > 10)
x = x + sum;
else
x = x * sum;
delta = (x + i * 12)/2;
И в этом участке лишним окажется оператор if, более того, значение переменной х так же оказывается предопределенным, и компилятор может счесть ее вовсе лишней, исключив и другие участки, где эта переменная задействована. В общем, может оказаться, что написанная с большими усилиями программа не делает ничего... Конечно, это свидетельствует в первую очередь о недостаточно хорошо продуманном алгоритме, либо о грубых ошибках программирования - к сожалению, интеллекта никакого оптимизатора не хватит найти и исправить такие ошибки.
С другой стороны, программист сам может предпринимать меры по оптимизации своей программы. Например, один из известных способов увеличения быстродействия программы (в ущерб ее размеру) заключается в принудительной замене вызовов функций их содержимым в каждом месте, где функция используется. Такой прием получил название встраиваемых в код функций, или (более традиционный термин) inline-функций.
Ключевое слово inline, использованное при описании функции, указывает компилятору (а точнее - его оптимизатору) при первой же возможности заменить обращения к функции явной вставкой в текст соответствующих операторов. В этом случае функция превращается в некое подобие макроса-функции. Однако само по себе ключевое слово inline еще ничего не гарантирует - компилятор все равно поступит так, как посчитает наиболее оптимальным.
Другая проблема, связанная с работой оптимизатора - это переменные, которые изменяют свое значение не в том модуле, где они описаны, а в другом, для которого они являются внешними. Например, в модуле interface.c, отвечающем за интерфейс с пользователем, определена переменная key для хранения кода нажатой клавиши, а значение в нее записывается в одной из функций модуля keyboard.c, отвечающего за опрос клавиатуры (в модуле keyboard.c переменная key описана как extern). В этом случае далеко не исключен (а точнее - закономерен) вариант, что при оптимизации модуля interfaces компилятор сочтет, что переменная key никогда не меняет своего значения, и исключит весь код, связанный с анализом ее значения. Естественно, получившаяся программа будет полностью неработоспособна.
Подобное поведение компилятора с оптимизатором породило массу мифов о «неправильных» компиляторах или ошибках в них. Однако, ошибка тут лишь одна - программиста. Чтобы оптимизатор умерил свою прыть и не трогал переменные, проанализировать поведение которых ему не по силам, программист обязан объявлять их как изменяемые, т.е. volatile. В этом случае и сама переменная, и связанный с нею код не будут «испорчены» оптимизатором.
Вариантов стратегии оптимизации программы, как правило, три: достижение минимума размера, достижение максимума скорости исполнения и универсальная, т.е. достижения оптимального соотношения размера и производительности. Обычно программисту предоставляются средства для более «тонкой» настройки оптимизатора под свои нужды. Кроме прочего, часто предлагаются средства отступить от стандарта Си или «обычного поведения» операторов и стандартных функций, чтобы выиграть в чем-то другом. С такими средствами программист должен быть особо осторожен, т.к. любое отступление от стандарта может повлечь проблемы. Однако грамотное использование всех средств позволяет достичь существенного выигрыша по всем статьям.
Далее в разделе «Оптимизация» рассмотрены основные средства оптимизации, которыми располагает программист при работе с компилятором GCC для микроконтроллеров AVR