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 说明符,具有在本地声明的变量且没有其他任何内容的代码实际上没有可观察到的行为,可以完全优化掉。因此,重要的是要重现此示例以全局变量(或通过其他方式在 inc
和 sum
上产生可观察的 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 执行向上转换。但是,我们还需要考虑加载和存储所需的周期数。 movem
比 movel
快。因此,无需使用 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_t
或 uint32_t
与 inc
之间存在巨大差异。在 8086 上,可以安全地假设使用 uint16_t
比 uint32_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
这样的优化级别至少会为给定函数生成最佳机器代码在小函数的情况下。
总的来说,我会首先为清晰起见进行优化,并以“自然”的大小声明数据。其次,我会为方便起见进行优化,并确保传递的数据以对开发人员方便的方式传递。第三,我会分析是否存在任何性能或占用空间问题,如果有,它们在哪里,然后才在分析器和反汇编的帮助下优化代码。
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 说明符,具有在本地声明的变量且没有其他任何内容的代码实际上没有可观察到的行为,可以完全优化掉。因此,重要的是要重现此示例以全局变量(或通过其他方式在 inc
和 sum
上产生可观察的 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 执行向上转换。但是,我们还需要考虑加载和存储所需的周期数。 movem
比 movel
快。因此,无需使用 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_t
或 uint32_t
与 inc
之间存在巨大差异。在 8086 上,可以安全地假设使用 uint16_t
比 uint32_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
这样的优化级别至少会为给定函数生成最佳机器代码在小函数的情况下。
总的来说,我会首先为清晰起见进行优化,并以“自然”的大小声明数据。其次,我会为方便起见进行优化,并确保传递的数据以对开发人员方便的方式传递。第三,我会分析是否存在任何性能或占用空间问题,如果有,它们在哪里,然后才在分析器和反汇编的帮助下优化代码。