16 位系统上的 32 位增量

32-bit increment on a 16-bit system

2 个代码部分中的哪一个在 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

没有答案。

通常 16 位运算在 16 位上更快,但这取决于编译器、优化和体系结构。

您至少必须编译它并签入程序集。

在那个特定的 case/example 中,您可以在两种情况下以单个 '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;
}

没有任何 storage-class 说明符,具有在本地声明的变量且没有其他任何内容的代码实际上没有可观察到的行为,可以完全优化掉。因此,重要的是要重现此示例以全局变量(或通过其他方式在 incsum 上产生可观察的 side-effect)。

摩托罗拉 68000

我们以摩托罗拉68000为例

在 16/32 位机器 Motorola 68000(甚至不是 68020)上,使用上述代码和全局变量反编译函数反编译为(由 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 执行向上转换。但是,我们还需要考虑加载和存储所需的周期数。 movemmovel 快。因此,无需使用 68000 用户手册进行实际的周期计数,我们可以说在 68000 上,这段代码的 uint32_t 变体更快,因为它需要更少的 3 个总线周期(没有 andil #65535,%d0 来执行)但是多了 2 个总线周期(两倍 movel from/to 内存而不是 movew),因此总共需要少 1 个总线周期。

然而,那是 1 CPU architecture/family 加上编译器,以及一种类型的情况,样本量完全不足以外推通用语句。

例如,当compiler/optimizer可以将andil #65535,%d0替换为extl %d0时,当类型从无符号变为有符号时,游戏已经改变。

英特尔 8086

在 Intel 8086 上,使用 Bruce 的 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

我们可以看到,在另一个 16 位 CPU 英特尔 8086 上,使用 uint16_tuint32_tinc 之间存在巨大差异。在 8086 上,可以安全地假设使用 uint16_tuint32_t 快得多。 32 位变体需要更多指令,甚至调用 off-screen 子例程来执行 32 位操作,如 inc 和 add。

另一个警告

以上分析是基于指令计数或总线取指计数。不考虑 CPU 使用的额外周期。但对于 real-world 场景,这很重要。应该使用分析器。

结论

这很CPU-dependent。不可能对所有 16 位平台进行通用说明。这在很大程度上取决于所讨论的 16 位平台如何处理 32 位操作数。在本例中,对 32 位操作数有强大支持的 16 位 CPUs,如摩托罗拉 68000,在 uint32_t 上的性能比 uint16_t 稍好。而不支持 32 位操作数的 16 位 CPUs,如 8086,在 uint32_t 情况下表现非常差,而在 uint16_t 情况下会好得多。

在某种程度上,它也取决于编译器,尽管我们可能想真诚地假设,像 GCC -O2 这样的优化级别至少会为给定函数生成最佳机器代码在小函数的情况下。

总的来说,我会首先为清晰起见进行优化,并以“自然”的大小声明数据。其次,我会为方便起见进行优化,并确保传递的数据以对开发人员方便的方式传递。第三,我会分析是否存在任何性能或占用空间问题,如果有,它们在哪里,然后才在分析器和反汇编的帮助下优化代码。