Типы данных и аргументы
Микроконтроллер 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-разрядная.