Типы данных и аргументы

Микроконтроллер 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-разрядная.


Изменено: