Инженер Фабьен ле Ментек (Fabien le Mentec) привел небольшое исследование, позволяющее лучше оптимизировать работу 8-разрядных контроллеров. Он работал над регулятором напряжения на основе 8-разрядного микроконтроллера ATMEGA328P фирмы ATMEL. Основная логика контроллера была реализована в главной функции main() и зависела от периодического таймера, функционирующего с фиксированной частотой. В обработчике прерывания по таймеру инкрементировалась переменная-счетчик, которая затем использовалась в основной программе для правильной в плане синхронизации работы контроллера напряжения.
Посмотрев на код Фабьена, один интересующийся человек отметил, что в этом коде для счетчиков используется тип uint8_t вместо unsigned int, и сказал, что с этим могут возникнуть трудности в рамках данного проекта. Но Фабьен пояснил причины и последствия такого решения.
В программе, на самом деле, имеется несколько счетчиков, некоторые из них необходимы для функционирования дополнительной логики. Но в данном случае речь пойдет только о счетчике в обработчике прерывания, выглядящем следующим образом:
#include <stdint.h>
#include <avr/io.h>
/* current version: */ static volatile uint8_t counter = 0;
/* initial version: static volatile unsigned int counter = 0; */
ISR(TIMER1_COMPA_vect)
{
/* ... */
if (counter != TIMER_MS_TO_TICKS(100))
{
++counter;
}
/* ... */
}
В связи с требованиями логики синхронизации и некоторыми ограничениями Фабьеном было принято решение об оптимизации кода обработчика прерываний, также он хотел найти способ сокращения времени цикла. Поэтому он предпочел использовать uint8_t вместо unsigned int, так как для данной платформы по умолчанию компилятор AVR-GCC выделяет под переменную типа integer 16 битов. Поскольку ATMEGA328P является 8-разрядным микроконтроллером, то 8-битная арифметика в данном случае позволяет получить более быстрый код.
Сравним две версии кода, представленные на ассемблере:
/* uint8_t version */
#include <stdint.h>
#include <avr/interrupt.h>
static volatile uint8_t counter = 0;
ISR(TIMER1_COMPA_vect)
{
/* ... */
/* avr-gcc -mmcu=atmega328p -O2 */
lds r24,counter
cpi r24,lo8(100)
brne .L1
lds r24,counter
subi r24,lo8(-(1))
sts counter,r24
.L1:
/* ... */
}
/* unsigned int version */
#include <avr/interrupt.h>
static volatile unsigned int counter = 0;
ISR(TIMER1_COMPA_vect)
{
/* ... */
/* avr-gcc -mmcu=atmega328p -O2 */
lds r24,counter
lds r25,counter+1
cpi r24,100
cpc r25,__zero_reg__
brne .L4
lds r24,counter
lds r25,counter+1
adiw r24,1
sts counter+1,r25
sts counter,r24
.L4:
/* ... */
}
Как и ожидалось, число инструкций в версии с uint8_t меньше. Кроме того, в руководстве, содержащем набор инструкций для 8-разрядных микроконтроллеров AVR приводится инструкция adiw, требующая два машинных цикла для своего выполнения. В итоге, версия с переменной count типа unsigned int в два раза больше версии с uint8_t.
Хотя при изменении типа переменной изменяется только одна строка кода, но под этим скрывается ряд важных последствий.
Во-первых, уменьшается емкость счетчика. В этом случае, конечно, нужно убедиться, что логика счетчика работает правильно при максимальном значении 0xff вместо 0xffff. И такой момент лучше прокомментировать, чтобы не упустить его в будущем, если придется возвращаться к программе.
Второе, менее очевидное последствие заключается в том, что такая оптимизация не работает на архитектурах, в которых размер арифметического слова превышает 8 бит. Программа будет компилироваться и работать, но такой шаг приведет к обратному эффекту, то есть в код будут добавлены дополнительные операции. Чтобы решить эту проблему, можно определить тип, ширина которого по умолчанию будет равна ширине слова текущей архитектуры:
#if defined(__AVR_ATmega328P__)
typedef uint8_t uint_word_t;
#else
typedef unsigned int uint_word_t;
#endif
Хотя можно поступить хитрее, поскольку компилятор C99 при подключенном файле stdint.h позволяет использовать тип int_fast8_t, который в данном случае будет «быстрейшим» целочисленным типом.
Перевод © digitrode.ru