32-битное приращение в 16-битной системе

Какая из двух частей кода будет выполняться быстрее на 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 имеет строгую типизацию, что способствует обнаружению ошибок во время компиляции.


70
2

Ответы:

Нет ответа.

Обычно 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).

Моторола 68000

Возьмем в качестве примера 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 при изменении типов с беззнаковых на знаковые.

Интел 8086

На 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, создаст оптимальный машинный код для данной функции, по крайней мере, в случае небольших функций.

В целом, я бы сначала оптимизировал для ясности и объявил данные в том размере, который они имеют «по своей природе». Во-вторых, я бы оптимизировал для удобства и удостоверился, что передаваемые данные передаются удобным для разработчиков способом. В-третьих, я бы профилировал, есть ли проблемы с производительностью или габаритами, и если да, то где они, и только потом оптимизировал код, с помощью профилировщика и дизассемблирования.