提供有用的指令是硬件的工作。编译器的工作是将抽象的高级语言翻译成这些指令。
硬件存储和操作位模式。只有在我们对其执行操作时,才会为位模式分配含义。如果我们对两个寄存器执行“有符号除法”操作,我们告诉硬件或库函数假定这些寄存器中的位表示有符号数。如果我们对两个寄存器执行加法操作,我们告诉硬件假定这些位模式表示数字,但我们并没有告诉它这些数字是有符号还是无符号。硬件没有理由知道或关心这一点。
二进制补码是有符号整数最流行的格式,因为它需要硬件的额外工作最少。特别地,如果使用模2n环绕(这是实现它们的最简单方法)来实现加法、减法和非扩展乘法,则这些相同的操作可以用于有符号和无符号算术。
尽管如此,对于有符号和无符号数,存在不同的操作,编译器或汇编程序员需要注意它们。
比较和溢出检测
在有符号和无符号算术中,相等比较是相同的。但是不等比较则不同。255毕竟大于零,而-1小于零。类似地,溢出检测也不同。在8位无符号算术中计算255 + 255会溢出,但在8位有符号算术中计算(-1) + (-1)则不会。
然而,大多数CPU没有专用的有符号和无符号比较操作。相反,大多数CPU都有一组标志,其中某些标志对无符号操作有意义,而不同的标志对有符号操作有意义。
这些标志可以作为常规算术操作的结果设置。大多数CPU还具有一个专用的比较操作,它有效地执行减法并设置标志,但丢弃结果。
例如,arm32的标志集合是(这些是相当典型的):
- Z:零标志,用于检测结果是否为零(对于比较指令,这表示操作数相等)
- C:进位标志,在将较大的算术操作构建成较小的操作时使用。在无符号算术中,也用于检测溢出。
- V:溢出标志,在有符号算术操作溢出时设置。
- N:负数标志,如果结果的最高位设置,则设置。在有符号算术操作的结果为负数时设置。
CPU并不知道或关心用户是执行有符号还是无符号操作,它总是根据一组简单的规则设置所有标志。编译器(或汇编语言编写者)需要知道哪些标志对其操作有意义,并选择适当的条件指令。
条件指令反过来会查看这些标志的各种组合。用于处理无符号值的条件指令查看C和Z标志,而用于处理有符号值的条件指令查看V、Z和N标志。
扩展
将数字从较小的类型转换为较大的类型称为扩展。对于无符号数,额外的位必须填充为零。对于有符号数,额外的位必须填充为符号位的副本。
一些体系结构可能具有专用的扩展指令,或者可能将扩展功能合并到其他指令中。例如,在32位arm上,所有算术指令都适用于32位字。在原始的arm CPU上,“加载字节”指令总是将字节零扩展。但是,在armv4中添加了其他加载指令,因此可以在单个操作中加载和扩展有符号字节和有符号和无符号半字。
同样,最近版本的32位ARM具有特定的符号扩展指令,它们丢弃寄存器的上半部分,并用下半部分的符号或零扩展替换其内容。
在其他情况下,可以使用更通用的指令进行扩展。这可能包括移位指令和按位操作。例如,将单个字值扩展为双字值可能使用比字大小少一位的移位(例如32位字大小的31)来构造一个新字,其中完全重复第一个字的符号位。
扩展乘法
扩展乘法通常产生比其参数大两倍的结果。例如,32x32扩展乘法通常会产生64位结果。在某些情况下,可能有一个单独的指令执行完整的扩展乘法,而在其他情况下,可能提供一个“高位乘法”指令,仅提供结果的顶部部分。
扩展乘法在许多应用中都很有用,包括定点算术和作为执行比字大小更宽的数字运算的构建块。
当处理器提供扩展乘法时,它们通常为有符号和无符号操作提供单独的版本。
位移
位移可用于将数字乘以或除以2的幂。
将数字“左移”以使其变大对于有符号和无符号数是相同的。然而,将数字“右移”以使其变小是不同的,对于无符号数,额外的位置应该填充零,而对于有符号数,应该用有符号位的副本来填充。
大多数CPU为有符号和无符号操作分别提供单独的右移指令。这些指令被称为“逻辑移位”(用于无符号操作),而对于有符号操作则称为“算术移位”(可能由于历史原因)。
请注意,一些高级语言,特别是C和旧版本的C ++,将负数移位实现的行为定义为实现定义。
除法
可以轻松看出,对于有符号和无符号数,除法是不同的。(-2)/(-1) = 2,但 254 / 255 = 0。
根据我的经验,许多CPU根本不提供任何除法指令,但当提供除法指令时,通常会提供有符号和无符号版本。