Какая из двух частей кода будет выполняться быстрее на 16-битной машине?
1:
uint16_t inc = 1;
uint32_t sum = 0;
inc++; // 16 bit increment
sum += (uint32_t)inc; // inc has to be cast
2:
uint32_t inc = 1;
uint32_t sum = 0;
inc++; // 32 bit increment
sum += inc; // no cast for inc
🤔 А знаете ли вы, что...
C имеет строгую типизацию, что способствует обнаружению ошибок во время компиляции.
Нет ответа.
Обычно 16-битная арифметика на 16-битной быстрее, но это зависит от компилятора, оптимизации и архитектуры.
Вы должны как минимум скомпилировать его и проверить в сборке.
В этом конкретном случае / примере вы можете закончить одной инструкцией «mov» в обоих случаях, потому что результат можно предсказать.
Это во многом зависит от конкретной 16-битной машины, а также от оптимизатора компилятора.
Чтобы имитировать действительно важное поведение программы, я поместил код внутрь функции, работающей с глобальными переменными.
Вариант 1 (uint16_t inc
):
#include <stdint.h>
uint16_t inc = 1;
uint32_t sum = 0;
void f() {
inc++;
sum += inc;
}
Вариант 2 (uint32_t inc
):
#include <stdint.h>
uint32_t inc = 1;
uint32_t sum = 0;
void f() {
inc++;
sum += inc;
}
Без каких-либо спецификаторов класса хранения код с переменными, объявленными локально и ничем другим, на самом деле не имеет наблюдаемого поведения и может быть полностью оптимизирован. Таким образом, для воспроизведения этого примера важно иметь глобальные переменные (или другими способами создавать наблюдаемый побочный эффект как для inc
, так и для sum
).
Возьмем в качестве примера Motorola 68000.
На Motorola 68000 (даже не 68020), которая является 16/32-битной машиной, дизассемблирование функции с приведенным выше кодом и глобальными переменными декомпилируется в (создается с помощью m68k-linux-gnu-gcc-12 -O2
):
Вариант 1 (uint16_t inc
):
00000000 <f>:
0: 3039 0000 0000 movew 0 <f>,%d0
6: 5240 addqw #1,%d0
8: 33c0 0000 0000 movew %d0,0 <f>
e: 0280 0000 ffff andil #65535,%d0
14: d1b9 0000 0000 addl %d0,0 <f>
1a: 4e75 rts
Вариант 2 (uint32_t inc
):
00000000 <f>:
0: 2039 0000 0000 movel 0 <f>,%d0
6: 5280 addql #1,%d0
8: 23c0 0000 0000 movel %d0,0 <f>
e: d1b9 0000 0000 addl %d0,0 <f>
14: 4e75 rts
Обратите внимание на дополнительную инструкцию andil #65535,%d0
для выполнения преобразования из d0 перед его добавлением. Однако нам также необходимо учитывать количество циклов, необходимых для загрузки и сохранения. movem
быстрее, чем movel
. Таким образом, не вдаваясь в фактический подсчет циклов с помощью руководства пользователя 68000, мы можем сказать, что на 68000 вариант uint32_t
этого фрагмента кода быстрее, так как он требует на 3 шинных цикла меньше (без andil #65535,%d0
для выполнения), но на 2 шины больше. циклов (дважды movel
из/в память вместо movew
), поэтому в сумме требуется на 1 шинный цикл меньше.
Однако это 1 архитектура/семейство ЦП плюс компилятор и одна ситуация с типами, совершенно недостаточный размер выборки для экстраполяции универсального оператора.
Например, игра уже меняется, когда компилятор/оптимизатор может заменить andil #65535,%d0
на extl %d0
при изменении типов с беззнаковых на знаковые.
На Intel 8086 с использованием Компилятор C Брюса (bcc) картина совсем другая.
Разборка, произведенная с помощью bcc -O -0 -S
, выглядит следующим образом.
Вариант 1 (uint16_t inc
):
.text
export _f
_f:
push bp
mov bp,sp
inc word ptr [_inc]
mov ax,[_inc]
mov bx,dx
mov di,#_sum
call laddul
mov [_sum],ax
mov [_sum+2],bx
pop bp
ret
Вариант 2 (uint32_t inc
):
.text
export _f
_f:
push bp
mov bp,sp
mov ax,[_inc]
mov si,[_inc+2]
mov bx,#_inc
call lincl
mov ax,[_sum]
mov bx,[_sum+2]
mov di,#_inc
call laddul
mov [_sum],ax
mov [_sum+2],bx
pop bp
ret
Мы видим, что на Intel 8086, еще одном 16-разрядном процессоре, существует огромная разница между использованием uint16_t
или uint32_t
для inc
. Можно с уверенностью предположить, что на 8086 использование uint16_t
будет намного быстрее, чем uint32_t
. 32-битный вариант требует значительно большего количества инструкций и даже вызывает вызовы внеэкранных подпрограмм для выполнения 32-битных операций, таких как inc и add.
Приведенный выше анализ основан на количестве команд или количестве выборок по шине. Дополнительные такты, используемые CPU, не учитываются. Но для реального сценария это важно. Следует использовать профайлер.
Это очень сильно зависит от процессора. Общее утверждение обо всех 16-битных платформах невозможно. Это во многом зависит от того, насколько хорошо рассматриваемая 16-битная платформа справляется с 32-битными операндами. 16-разрядные процессоры с сильной поддержкой 32-разрядных операндов, такие как Motorola 68000, в этом примере будут работать немного лучше на uint32_t
, чем на uint16_t
. В то время как 16-битные процессоры, которые вообще не поддерживают 32-битные операнды, такие как 8086, работают очень плохо в случае uint32_t
и будут намного лучше в случае uint16_t
.
В некоторой степени это также зависит от компилятора, хотя мы могли бы добросовестно предположить, что уровень оптимизации, подобный GCC -O2
, создаст оптимальный машинный код для данной функции, по крайней мере, в случае небольших функций.
В целом, я бы сначала оптимизировал для ясности и объявил данные в том размере, который они имеют «по своей природе». Во-вторых, я бы оптимизировал для удобства и удостоверился, что передаваемые данные передаются удобным для разработчиков способом. В-третьих, я бы профилировал, есть ли проблемы с производительностью или габаритами, и если да, то где они, и только потом оптимизировал код, с помощью профилировщика и дизассемблирования.