title date categories tags
Number Systems
2023-08-13 14:30:00 +0800
Computer Systems
computer systems

计算机硬件使用 0 和 1 来存储信息。当我们需要存储的信息为数字(Number)时,我们可以使用不同的编码方式(Encoding)将相同的一个数字存储为不同的 01 排列,以满足不同的需求。同样,在面对计算机硬件中已经存在的 01 排列时,我们可以使用不同的解码方式(Decoding)将相同的 01 排列理解为不同的数字。

本文将对常见的整数编码解码方式(又称表示方法,Representation)进行梳理,并重点关注它们是如何从需求中诞生的。

Bits/Context

在所有一切讨论开始以前,我们需要区分清楚两个概念:01 排列和编码解码方式。

单单从计算机硬件角度看,硬件中的 01 排列是没有意义的,它们只是单纯的 0 和 1。同样,硬件对 01 排列进行的一系列运算也是没有意义的,硬件只是根据数字电路的设计机械地对输入的 01 排列进行处理,最后输出另一串 01 排列。

01 排列的意义是由人类赋予的,准确来说,是编码解码方式赋予的。编码解码方式规定了人类在看到一串特定的 01 排列后,该用怎么样的方式进行理解。

Unsigned Numbers

面对一堆 0 和 1,最自然的解码方式就是把这些 0 和 1 当作一个正常的二进制数(Binary Number)。比如看到 01 排列 0101,我们会自然地将其当成二进制数 0101。

Addition

当我们想相加两个数字 0001 和 1001 的时候,我们很自然会使用二进制加法:

  1
0001
1001
----
1010

这样的二进制加法在计算机中是使用数字电路实现的。这样进行二进制加法的数字电路叫做加法器。

Drawback

我们很快会发现,这样的编码方式只能对正数进行编码,不能对负数进行编码。

为了表示负数,我们需要用一种方式在 01 的排列中表示出正负。

Sign/Magnitude Numbers

正负是 2 种状态,一位 0 和 1 也是 2 种状态。所以我们可以在 01 排列中用一位表示正负信息,用剩余的 01 排列表示绝对值大小。比如规定最高位表示正负信息,0 表示正,1 表示负,剩下的几位表示绝对值。这就是 Sign/Magnitude 编码方式。

用这种编码方式,-1 将被表示为 1001,1 将被表示为 0001,非常直观。

Drawback

但是这种编码方式有几个问题:

Ones' Complement

两个例子

由变换得到编码方式

取值范围

我们限定 01 排列是 8 位的,那么 -0 的编码就是 1111 1111,-1 的编码就是 1111 1110,-127 是 1000 0000,-128 是 0111 1111;+127 是 0111 1111,+128 是 1000 0000。可以看到,产生了重复。为了根据最高位判断符号,所以 8 位 01 排列用这种解码方式取值范围是 -127~+127。

另外主要注意,0 有两种表示方法。

另外三种情况

上面的两个例子中,我们已经讨论了正加负等于正,以及正加负等于负这两种情况,整数相加还有下面三种情况:

优点和缺点

如果使用 Ones' Complement 编码解码方式,进行加法和减法都只需要加法器和取反器,相比 Sign/Magnitude 简单了太多。另外,Ones' Complement 也可以通过最高位判断正负。

但是,Ones' Complement 还是存在正 0 和负 0 的问题。

我们可以尝试一下,看看能不能找到一种编码解码方式,能完美解决 Sign/Magnitude 的两个缺点。

Two's Complement

改进变换方式

Ones' Complement 编码方式中 0 有两种编码。我们尝试对编码方式进行更改,让 0 只有一种编码。

我们可以对负数绝对值取反后再加一个 1,这样 -0 的 Ones' Complement 编码 1111 1111 就变成 0000 0000 了。+0 的编码还是 0000 0000。整数 0 就只有一种编码了。

接下来,我们先用这种变换方式尝试一下常见的整数运算情况。

两个例子

由变换得到编码方式

取值范围

-1 是 1111 1111,-127 是 1000 0001,-128 是 1000 0000,-129 是 0111 1111。

+1 是 0000 0001,+127 是 0111 1111,+128 是 1000 0000。

为了保证负数最高位是 1,取值范围为 -128~+127。

另外需要注意,整数 0 的编码只有 0000 0000。

另外三种情况

上面的两个例子中,我们已经讨论了正加负等于正,以及正加负等于负这两种情况,整数相加还有下面三种情况:

0 参与运算

上面已经说明,0 的 Two's Complement 编码是 0000 0000。用这个编码输入加法器,0 加 0,0 加负数,0 加正数都不需要进行讨论。

但是 0 的编码实际上是 1111 1111 - 0000 0000 + 1 得来的,我们下面讨论一下这种编码方式的 0 参与运算的情况:

溢出

当加法器的输出对应的数字解码后不符合计算结果时,就是溢出。

优点

通过上面的讨论,我们可以看到 Two's Complement 方式完美解决了 Sign/Magnitude 的两个缺点。而 Two's Complement 也是所有现代计算机编码有符号数的方式。

从源代码的加减到加法器

我们可以思考一下,运算操作最初的呈现形式是怎么样的。

最开始,一个程序员在源代码中,通过各种途径(初始化、计算、外界输入等)把两个值放在两个变量里,并对这两个变量进行加法或者减法,结果放在另一个变量里。即 c = a +/- b;。很明显,a 和 b 是有类型的,它们的类型规定了它们的大小和是否带符号。而通过类型大小和是否带符号,我们可以得到 a 和 b 的取值范围。

接下来,源代码会转化为汇编代码。c = a + b 会变成 add rc, ra, rbc = a - b 会变成 sub rc, ra, rb。值得注意的是,如果变量 a 或 b 的类型是 signed,那么寄存器 ra 或 rb 的二进制是变量 a b 值的 two's complement 编码。如果变量 a 或 b 的类型是 unsigned,那么寄存器 ra 或 rb 的二进制是变量 a b 值的 unsigned 编码。

所以,如果变量 a b 是 signed,它们对应的加减汇编指令一共有这些情况:add 正 正;add 正 负;add 负 正;add 负 负;sub 正 正;sub 正 负;sub 负 正;sub 负 负。

如果变量 a b 是 unsigned,它们对应的加减汇编指令一共有这些情况:add rc, ra, rb;sub rc, ra, rb。

signed 操作

先看 signed,我们已经讨论过加法的三种情况,现在讨论减法的四种情况。

unsigned 操作

然后再来看 unsigned。两个 unsigned 变量的加法很直观,直接喂给加法器就可以。如果加法器有 carry out,那么就有溢出。

重点是减法。下面对 unsigned number msb 是否是 1 分四种情况讨论。