04 Signed and Unsigned Numbers

人有十个手指,所以计数时采用十进制(decimal)。计算机使用高低电信号表示数,所以很自然地采用了二进制(binary)。

一个二进制数的一位成为二进制数字(binary digit),或者比特(bit)。

一般地,数字用任意底表示,第 位的数字 的大小是 其中 是从 0 开始且从右往左数。利用这个特性,可以实现不同底之间的数字互转 我们从右往左依次标记 ,那么 RISC-V 中 的二进制表示如下图。

由于每个比特表示的数大小不同,所以我们将最右边的位称为最低位(least significant bit),最左边的位称为最高位(most significant bit)。

RISC-V 的字有 32 比特,那么可以表示 个不同的数,从 0 到

32 比特的数表示的十进制是

这些正数称为无符号数。

由于二进制对人而言不如十进制自然,所以早期的计算机使用十进制表示,不过高低电平只能表示两种状态,所以使用若干比特表示一个十进制数字。但是这种表示方式非常不高效,所以后续计算机采用二进制表示,只有在输入输出这些必要的地方才做转换。

如果一个数很小,那么硬件的高位都是 0。第三章会将二进制的加减乘除,不过这里需要提一下溢出(overflow),即结果无法用硬件有限的比特数表示。溢出发生时的行为依赖于编程语言、操作系统和程序期待的行为。

计算机需要计算正数,也需要计算负数,所以需要某种方式表示负数。一个直观的方式是分离符号位和数值位。

这种表示方式有以下缺点。用哪一位表示符号呢?早期计算机常识最左边和最右边两种方式。由于提前不能知道符号是正还是负,所以加法需要一个额外的步骤设置符号位。对于粗心的程序员,这种方式可能会导致一些问题。因为这些缺点,符号和数值分离的方式被淘汰了。

一个很小的无符号数减去一个更大的无符号数,得到的结果前面有一串 1,这启发研究人员发明了二的补码(two's complement)表示,后面称为二进制补码。开头(最高位,最右边)是零表示整数,一表示负数。

这个名字的来历是 比特数和其负数的无符号和是 ,即 的补或者其负数是

正数的范围从 ,负数范围从 。范围不平衡,负数表示范围多了一个数,这会给粗心的程序员带来困扰。不过符号和数值分离的方式不仅会给程序员带来困扰,也会给硬件设计者带来困扰。

一个替代符号和数值分离的表示方法是一的补码。一个数的负数是把所有比特翻转,零变成一,一变成零,这样 的补是 。这样表示的好处是由于存在两个零(全零是正零,全一是负零),正数和负数范围恰好对称。不过由于一的补码的加法需要一个额外步骤——减一,所以也被淘汰了。

如果需要知道一个数是整数还是负数,只需要检测最高位即可,因此这一位也称为符号位(sign bit),但是和符号和数值分离表示法中的符号位意义不太一样。

32 比特有符号数的二进制表示的十进制是

和无符号数一样,有符号数的计算也会溢出。此时,最高位(符号位)与预期相反:正确结果为负但是符号位是零,反之亦然。

对于 load 指令来说,符号拓展(sign extension)是十分必要的,目的是把数正确地加载到寄存器中。无符号数前面补零,有符号数补符号位。32 位的寄存器,加载 32 位整数,那么有符号数和无符号数是一样的。不过对于更短的类型,比如字节,lbulb 的行为就不一样了。

编程语言要能够区分无符号数和有符号数。C 语言的有符号整数是 int,而无符号数是 unsigned int,有时前者写作 signed int 以保持一致。

下面分析两个处理二进制补码的便捷方法。

第一个是求一个二进制数的相反数。方法是所有位取反,然后最后结果加一。原理是一个数与所有位取反之后的数相加的二进制全都是一,即 -1,这里用 表示将 所有位取反的数,那么 ,稍作调整 所有 的相反数是

第二个是符号拓展,可以将 位二进制数拓展成比 位多的二进制数。方法如前所述,无符号前面补零,有符号补符号位。