Программирование ARM Keil: уровни оптимизации компилятора и отладка Mon, September 27 2021  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Keil: уровни оптимизации компилятора и отладка Печать
Добавил(а) microsin   

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

0: минимальная оптимизация (опция -O0). Большинство оптимизаций выключено. Когда разрешена отладка, этот вариант опции оптимизации дает самый лучшую отладку по исходному коду и просмотру переменных (debug view), потому что структура генерируемого кода точно соответствует исходному коду. Вся оптимизация, которая мешает отладочному представлению, отключена. В частности:

• Точки останова (breakpoints) могут быть остановлены только в любое достижимое место, включая "мертвый" код.
• Значение переменной доступно в любом месте области её действия, кроме места, где она не инициализирована.
• Обратная трассировка (backtrace) предоставляет стек открытых активаций функций, как это ожидается от чтения исходного кода.

Замечания:

1. Хотя вид режима отладки, предоставляемый опцией -O0, наиболее близко соответствует исходному коду, пользователи могут предпочесть режим отладки опции -O1, потому что этот вариант оптимизации улучшает качество кода без изменения его фундаментальной структуры.
2. К "мертвому" коду относится код, по которому проходит выполнение, но этот код никак не оказывает влияние на результат работы программы. Например, это может быть присваивание значения локальной переменной, которая нигде не используется. "Недостижимый" код это такой код, по которому выполнение никогда не проходит. Например, это может быть код, непосредственно следующий за оператором return.

1: ограниченная оптимизация (-O1). Компилятор выполняет только оптимизацию, которая может быть описана отладочной информацией. При этом из кода удаляются не используемые inline-функции и не используемые static-функции. Отключение оптимизаций серьезно ухудшает качество режима отладки (debug view). Если опция -O1 используется вместе с опцией -debug, то этот вариант в основном дает удовлетворительную возможность отладки с хорошей плотностью кода.

Отличия режима отладки опций -O1 и –O0:

• В "мертвом" коде нельзя поставить точки останова.
• Значения переменных могут быть недоступны в их области действия после того, как эти переменные были инициализированы. Например, если повторно используется место их размещения.
• Функции, которые не дают побочных эффектов, могут быть вызваны не в той последовательности, как они появляются в коде, или их вызов может быть опущен, если результат вызова не нужен.
• Обратная трассировка (backtrace) может не дать информации по стеку открытых активаций функций, как это ожидалось бы при чтении кода.

Уровень оптимизации –O1 дает хорошее соответствие между исходным кодом и объектным кодом, особенно если в исходном коде нет "мертвого" кода. Объем генерируемого кода может быть значительно меньше, чем код опции –O0, которая может упростить анализ объектного кода.

2: высокий уровень оптимизации (-O2). Если используется опция --debug, то вид режима отладки может быть менее, чем удовлетворительный, потому что не всегда соблюдается соответствие между объектным кодом и исходным кодом. Компилятор может выполнить оптимизации, которые не могут быть описаны отладочной информацией.

Опция -O2 это уровень оптимизации по умолчанию. Отличия режима отладки от опции –O1:

• Соответствие исходного кода объектному коду может находиться по принципу "многое к одному", потому есть возможность повторений исходного кода в разных местах, которые будут преобразованы к одному месту в файле, и применяется более агрессивное планирование выполнение инструкций.
• Планирование выполнения инструкций разрешено для перекрестных точек в последовательности. Это может привести к несоответствию между сообщаемым значением в переменной на определенном месте кода, и значением, которое можно было бы ожидать при чтении исходного кода.
• Компилятор автоматически выполняет встраивание функций (короткие функции могут быть преобразованы в inline-код).

3: максимальная оптимизация (-O3). Когда отладка разрешена, эта опция обычно дает плохую отладку по исходному коду. ARM рекомендует применять более слабые уровни оптимизации.

Если Вы совместно используете опции -O3 и -Otime, то компилятор будет выполнять дополнительные оптимизации, которые будут более агрессивны, такие как:

• Высокоуровневые скалярные оптимизации, включая разворачивание циклов. Это может дать значительный выигрыш в производительности ценой небольшого увеличения размера кода, но присутствует риск увеличения времени сборки.
• Более агрессивное встраивание функций (inlining) и применение автоматического встраивания функций (automatic inlining).

Эти оптимизации эффективно меняют исходный входной код, в результате чего объектный код получает наименьшее соответствие исходному коду, и получается самый плохой debug view. Опция --loop_optimization_level=option управляет величиной оптимизации цикла, которая применяется опциями –O3 –Otime. Чем больше величина оптимизации цикла, тем хуже соответствие между исходным и объектным кодом.

Для дополнительной информации по высокоуровневым трансформациям исходного кода при опциях –O3 –Otime используйте опцию командной строки --remarks.

Из-за того, что оптимизация влияет на соответствие объектного кода исходному коду, выбор уровня оптимизации -Ospace и -Otime обычно влияет на поведение режима отладки (debug view).

Опция -O0 самый лучший вариант из всех, если требуется упрощенный debug view. Выбор -O0 обычно увеличивает размер образа ELF по объему от 7 до 15%. Чтобы уменьшить размер отладочных таблиц, используйте опцию --remove_unneeded_entities.

[Результаты тестирования опций оптимизации Keil]

Мне было интересно узнать, как влияют опции оптимизации -O0, -O1, -O2 и -O3 на скорость выполнения кода и на затраты памяти под код и переменные. Проверял на Keil uVision 5.34.0.0, сравнивал время выполнения задержки на простом цикле и размер затраченных ресурсов в проекте. Результаты свел в таблицу:

Опция Время прокрутки цикла Code, байт RO-data, байт RW-data, байт ZI-data, байт
-O0 2.2 мс 11120 840 740 15596
-O1 44.4 мс 9616 840 732 15600
-O2 44.4 мс 9320 840 732 15600
-O3 44.4 мс 9328 840 732 15600

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

[-Otime]

Выполняет оптимизацию для уменьшения времени выполнения ценой возможного увеличения объема кода.

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

Если с опцией -Otime компилируется следующий код:

while (expression)
   body;

В результате получится следующий преобразованный код:

if (expression)
{
    do body;
    while (expression);
}

[-Ospace]

Выполняет оптимизацию для уменьшения объема кода ценой возможного увеличения времени его выполнения.

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

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

[#pragma On, #pragma Otime, #pragma Ospace]

Существует возможность с помощью директивы #pragma изменить поведение оптимизатора компилятора, которое было задано в командной строке. Например, если в командной строке была опция -O0 (не применять оптимизацию), и посередине модуля применить директиву #praqma -O3, то все функции за этой, до конца файла, директивой будут скомпилированы с максимальным уровнем оптимизации.

Ниже показан пример компиляции модуля кода с функциями function1, function2 и function3, когда компилятор armcc был вызван с опцией -O0. Здесь function1 будет скомпилирована без оптимизации, а функции function2 и function3 с максимальным уровнем оптимизации.

void function1(void){
    ...                // Без оптимизации (armcc -O0)
}
 
#pragma O3
void function2(void){
    ...                // Оптимизирована на уровне O3
}
 
void function3(void){
    ...                // Оптимизирована на уровне O3
}

[#pragma push, #pragma pop]

Если необходимо изменить поведение оптимизатора компилятора только для одной функции в модуле, то необходимо использовать директиву #pragma On (либо #pragma Otime, либо #pragma Ospace) в обрамлении директив #pragma push и #pragma pop.

Например, когда модуль компилировался с командной строкой armcc -Ospace (по умолчанию):

void function1(void){
    ...                 // Оптимизирована по размеру кода (armcc -Ospace)
}
 
#pragma push
#pragma Otime
void function2(void){
    ...                 // Оптимизирована по времени выполнения
}
#pragma pop
 
void function3(void){
    ...                 // Оптимизирована по размеру кода (armcc -Ospace)
}

[Ссылки]

1. Compiler optimization levels and the debug view site:keil.com.

 

Добавить комментарий


Защитный код
Обновить

Top of Page