AVR4027: Трюки и советы по оптимизации Си кода для 8-и разрядных AVR микроконтроллеров. Ч.2

15/09/2013 - 20:29 Павел Бобков

4. Советы и трюки по уменьшению времени выполнения кода

   В этом разделе мы рассмотрим советы по уменьшению времени выполнения кода. Для каждого совета будет дано описание и пример.

4.1 Совет #8 - размеры и типы данных

   Правильный выбор типа данных уменьшает не только размер кода, но и время его выполнения. Для 8-и битных AVR использование однобайтных значений – это наиболее эффективный путь. 


Таблица 4-1. Пример использования различных типов данных.


   Обратите внимание, что при оптимизации -O3 цикл автоматически развернется компилятором в повторяющиеся операции и разницы между примерами не будет.

4.2 Совет #9 – условный оператор

   Обычно между предекрементом и постдекрементом (или преинкрементом и постинкрементом) нет никакой разницы, если эти операторы используются отдельно. Например “i- -;“ и “- -i;” преобразуются компилятором в одинаковый ассемблерный код. Однако если эти операторы используются в составе циклов или условных операторов, то сгенерированный код будет разным.

   Как говорилось в совете #3, использование в цикле декремента индексной переменной уменьшает размер кода. Это также помогает получить более быстрый код и в условных операторах.

   Кроме того, предекремент и постдекремент дают разные результаты. Из примера в таблице 4-2 мы можем видеть, что более быстрый код получается при использовании в условном оператора предекремента. Значение счетчика циклов здесь представляет время выполнения длиннейшей итерации цикла.

Таблица 4-2. Пример условного оператора


   Переменной “loop_cnt” присваиваются разные значение, но поведение программы будет одинаковым: PORTC переключается 9 раз, а PORTB за это время 1 раз.

4.3 Совет #10 – развертывание циклов

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

   В таблице 4-3 показан пример, где мы инвертируем один вывод порта 7 раз.

Таблица 4-3. Развертывание циклов.


   Заменив цикл последовательностью операций, мы увеличили скорость выполнения кода с 80 циклов до 50. Однако имейте в виду, код в этом случае увеличится с 94 байтов до 140. 

   Также следует заметить, если для левого примера включить оптимизацию –O3, компилятор развернет цикл автоматически.

4.4 Совет #11 – управление потоком: if-else и switch-case

   “If-else” и”switch-case” широко используется в Си и правильная организация ветвлений может увеличить быстродействие кода.

   Для “if-else” размещайте наиболее вероятное условие на первое место. Тогда следующие условия будут выполняться реже и среднее время выполнения кода уменьшится.

   Используя “switch-case” можно устранить недостатки “if-else”, потому что для “switch-case” компилятор обычно создает таблицу перекодировки с индексами и переходит на нужную ветвь напрямую (без последовательного перебора).

   Если использовать “switch-case” сложно, мы можем разделить “if-else” ветви на более мелкие подветви. Этот метод снижает время выполнения кода для наихудшего случая. В примере ниже, мы получаем данные с АЦП и затем передаем их с помощью USART. “ad_result <= 240” – это наихудший случай.

Таблица 4-4. Пример использования “if-else” подветвей




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

5. Пример проекта и проверка результата

   К этому документу прилагается тестовый проект, в котором используются некоторые описанные трюки и советы (но не все). В проекте включена оптимизация по размеру кода –Os.

   Что делает эта программа? Микроконтроллер использует один канал АЦП чтобы оцифровывать напряжение на входе, полученный результат он отравляет по USART`у каждые пять секунд. Если результат АЦП выходит за диапазон, то в течении 30 секунд посылается сигнал аварии и устройство переходит в режим ошибки.

   Размер кода и его быстродействие до и после оптимизации приведены в таблице 5-1.

Таблица 5-1. Результаты оптимизации тестового проекта.



* один цикл - 5 выборок АЦП и одна USART передача

6. Заключение

   В этом документе мы перечислили некоторые советы и трюки, позволяющие увеличить эффективность Си кода. Спасибо современным компиляторам, они достаточно умны и применяют различные оптимизации автоматически. Однако ни один компилятор не знает код лучше разработчика, поэтому важно создавать хороших код.

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

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

Ссылки

Atmel AVR4027: Tips and Tricks to Optimize Your C Code for 8-bit AVR Microcontrollers
AVR4027: Трюки и советы по оптимизации Си кода для 8-и разрядных AVR микроконтроллеров. Ч.1
Проект к статье - avr4027.zip


Вольный перевод - ChipEnable.Ru

Comments   

# neiver 2013-09-17 15:51
Еще из таких микрооптимизаци й можно вспомнить замену логических операций на побитовые.
Логический оператор И:
Code:void Bar(uint8_t a, uint8_t b){
if(a && b){
PORTB = 0;
PORTA = 0;
}
}

Code:138: 88 23 and r24, r24
13a: 21 f0 breq .+8
13c: 66 23 and r22, r22
13e: 11 f0 breq .+4
140: 18 ba out 0x18, r1
142: 1b ba out 0x1b, r1
144: 08 95 ret

Побитовый оператор И:
Code:void Bar(uint8_t a, uint8_t b){
if(a & b){
PORTB = 0;
PORTA = 0;
}
}

Code:138: 68 23 and r22, r24
13a: 11 f0 breq .+4
13c: 18 ba out 0x18, r1
13e: 1b ba out 0x1b, r1
140: 08 95 ret

В данном случае разница в 4 байта кода и 0-3 такта времени.
Только надо учитывать, что такая оптимизация не всегда применима и выгодна. В данном случае a и b должны быть нормализованным и - 0 или 1.
Еще логический оператор ленивый, он не вычисляет b если a ложно.
# САБ 2013-09-18 09:41
Совет №8 слабоват.
Во-первых, чтобы эффективно использовать возможность Си компилировать один и тот же исходник для разных архитектур для большинства переменных (а для временных - вообще для всех) нужно использовать типы uintXX_fast_t (в конкретно этом примере надо использовать тип uint8_fast_t). Для AVR разницы не будет, а вот при использовании этого же кода на 16- или 32-битном процессоре разница уже будет ощутима.
# САБ 2013-09-18 09:48
"постинкремент" , "преинкремент" пишутся слитно, а "преддекремент" - слитно и с двумя "д".
Опечатки: 4.2 "получается при использование", 4.4 - "мы может разделить"
# САБ 2013-09-18 09:49
"постдекремент" тоже пишется слитно, разумеется
# САБ 2013-09-18 09:53
Хотя насчет двух "д" что-то я стал сомневаться.
# Pashgan 2013-09-18 12:04
Спасибо. Поправил (сейчас не видно, потому что страница кэширована).

У вас недостаточно прав для комментирования.