Типы данных и аргументы
Микроконтроллер stm32f103c8 имеет 32-битное ядро Cortex-M3 (ARMv7-M) и, следовательно, наиболее эффективно работает с 32-битными данными. Другими словами, лучше избегать char и short в качестве локальных переменных. Исключением будут ситуации, когда программист сознательно использует такое свойство типа данных. Например, 8-битный тип данных char при переходе через 255 даст 0.
Рассмотрим следующий код:
for (char i = 0; i < 127; i++) {}
На первый взгляд может показаться, что использование char более выгодно, так как он займет меньше места в памяти или в регистре процессора, чем обычный int. Это предположение ошибочно: все регистры являются 32-битными, в стеке размерность данных тоже 32 бита. Посмотрим вывод ассемблерного кода (без оптимизации):
MOVS R0, #0
B.N 0x80000f4
ADDS R0, R0, #1
UXTB R0, R0
CMP R0, #127
BLT.N 0x80000f2
И точно такой же код, но с использованием 32-битной переменной (uint32_t):
MOVS R0, #0
B.N 0x8000100
ADDS R0, R0, #1
CMP R0, #127
BCC.N 0x80000fe
Вдаваться в смысл каждой инструкции не нужно, здесь важно их количество: при использовании 8-битной переменной компилятор генерирует на одну строчку больше. По сути UXTB R0, R0 эквивалентна записи:
i = (char) i;
Как вы понимаете, программа совершит на 127 операций больше, чем при использовании 32-битной переменной. При использовании оптимизации (ключа) компилятор самостоятельно изменяет тип переменной, увеличивая тем самым производительность.
Аналогичная проблема возникает и с аргументами функций. Дело в том, что сами аргументы передаются в регистры ядра, о которых мы говорили ранее, — r0, r1, r2, r3, а они являются 32-битными. Допустим, у нас есть некая функция:
short vars_in_func_1(short a, short b) {
return a + (b >> 1);
}
Аргументы складываются в 32-битные регистры, при этом программист сам не следит за тем, чтобы возвращаемое значение находилось в пределах [-32768, +32767]. Этим снова займется компилятор, добавив лишнюю инструкцию.
В операциях сложения, вычитания и умножения разницы в производительности знаковых и беззнаковых переменных нет. Разница появляется при делении.
int mean_signed(int a, int b) {
return (a + b) / 2;
}
Компилятор добавляет единицу к сумме перед смещением в том случае, если сумма отрицательна. Например, x / 2 заменяется на выражение:
(x < 0) ? ((x + 1) >> 1) : (x >> 1)
Это происходит потому, что переменная x знаковая. Например, -3 >> 1 = -2, но -3 / 2 = -1. Более того, в Cortex-M3 присутствует инструкция деления (SDIV для знакового и UDIV для беззнакового), в то время как в Cortex-M0, например, ее нет. Замерить производительность вы можете, используя системный таймер SysTick.
Хорошим тоном в программировании встраиваемых систем является точное указание размера (т.е. и диапазона) переменной. В этой связи unsigned int лучше заменять на uint32_t. При переносе кода, скажем, на 8-битный микроконтроллер код продолжает исправно работать, так как uint32_t — аппаратнонезависимый тип. Если есть необходимость писать код для различных микроконтроллеров, то более эффективным будет использование типов с суффиксом _fast. Фактическая разрядность переменной типа uint_fast16_t различается для различных контроллеров, но всегда не меньше 16 бит. Например, для 32-битных ARM это будет 32-разрядная целочисленная переменная, а для AVR — 16-разрядная.
