最近在读《深入理解计算机系统》(CSAPP), 第二章中关于补码的描述很有意思, 在书中并没有详细叙述补码的由来和为什么要使用补码来表示有符号数, 而不是用原码和反码. 相反这本书详细的叙述了补码的数学表示, 以及公式的推导! 对补码的由来却一笔带过, 甚至原码和反码只是简单的在后面的篮框提示中提了一下, 根本没有出现在正文.
这在一定程度上造成了我的阅读困难, 于是在搜索引擎的帮助下, 我查了很多资料, 了解到补码的更多细节, 以及这个神奇的编码方式如何帮助人们设计 CPU 时节省大量的精力和金钱. 这里我把他记录下来.
计算机中的信息都是用二进制表示的, 在表示无符号数时并没有什么问题, 但是在表示有符号整数时就出现了问题, 如何在二进制中区分正数与负数呢?
区分正数和负数的另一个意义在于如果能够准确的表示出, 负数, 我们就可以化减法为加法, 如 5-4 就可以表示为 5+(-4), 至于为什么那么多先辈要执着于把减法化为加法, 原因很简单, 对计算机的电路而言, 计算加法要比计算减法方便的多.
很多人提出了有符号整数的编码方式, 其中有: 原码(Sign-Magnitude), 反码(ones'Complement), 补码(two's Complement).
原码
原码的英文为 sign-magnitude, 第一个单词为符号的意思, 第二个单词经过查询有 "大小" 的意思, 大意应该为符号 - 数值的组合, 在原码中, 第一个数字代表符号位, 1 代表负数, 0 代表正数, 余下的为数值位, 用来表示具体的数值. 这种编码方式最简洁易懂, 也最符合人的思维. 比如:-5 用 4 位二进制原码来表示就是 1101.
CSAPP 中给出的原码数学公式为:
由此我们能推断出 SMAXw=2w-1-1,SMINw=-(2w-1-1). 如八位二进制原码的范围应该为[-127,127].
使用原码来让机器做运算似乎是可行的, 但是实际上, 这种方法对机器来说却是十分困难的, 机器无法像人一样判断出 0 是 +,1 是 -, 设计出一个能让机器判断正负的电路也是十分复杂和困难的.
实际上目前我们很少用原码来做运算, 原码目前常常用来作为我们求补码的一个过渡.
反码
反码的英文为 ones'complement, 乍看之下有些迷惑, 翻译过来是" 很多个 1 的补 ", 似乎更迷惑了...
其实反码确实可以用 "很多 1 的补" 来理解, 我们都知道 w 位二进制数表示的最大范围是 2w-1, 用位向量来表示就是[111111....11111], 就是很多个 1. 假设 x 是正数, 求 - x 的补码的含义实际上就是求[11111...111]-x, 也就是(2w-1-x)(其中 w 为位数), 这是反码的一种计算方式, 另一种计算方式就是用原码计算, 原码的符号位不变, 剩下的数值位全部取反, 比如 1011 的反码就位 1100. 这和上面的计算方法得到的结果是一致的.
当然要注意的是上面所说的计算方法只对负整数有作用, 正整数的反码实际上和正常的无符号数编码没有区别.
反码的另一种具体数学解释是在 CSAPP 中看到的:
由此我们能推断出 OMAXw=2w-1-1,OMINw=-(2w-1-1). 如八位二进制原码的范围应该为[-127,127].
这个公式应该可以通过上面的提到的计算方法严格证明出来. 使用此公式可以快速的算出反码表示的值.
反码的优点是计算快捷, 不仅仅是对人而言, 计算一个负数的补码, 只需要将其的正数表示按位取反即可, 对于机器来说, 取反可比做加减法快多了.
反码实际上可以用来做二进制运算, 它的规则是从低位到高位逐列进行计算. 1 和 1 相加是 0 但要产生一个进位 1,0 和 1 相加是 1,0 和 0 相加是 0. 若最高位相加后产生进位, 则最后得到的结果要加 1.(反码运算的原理将在下面的补码运算中解释).
需要注意的是与原码不同的是, 反码的运算符号位与数值一起参加运算.
用反码运算, 其运算结果亦为反码. 在转换为真值时, 若符号位为 0, 数位不变; 若符号位为 1, 应将结果求反才是其真值.
原码和反码的缺陷
原码和反码在不考虑性能的情况下确实能够用来做二进制有符号整数的计算, 历史上也确有基于这两种编码方式的机器. 但是这两种编码却存在很多难以解决的缺陷.
考虑四位二进制数 [0000], 在原码中表示 0, 在反码中也表示 0, 而对于原码来说[1000] 也表示 0, 对于反码来说 [1111] 也表示 0. 一般我们把 [0000] 称为 + 0, 而另一种表示方法称为 - 0.
一个数字有两种编码方式, 这显然是不合理的!
为了解决这种不合理我们引入了补码的概念, 它继承了反码易于计算的优点, 也解决了原码难以计算的问题.
介绍完了原码反码, 在介绍补码之前, 为了便于理解, 我们首先要介绍一下模的概念:
模
"模" 是指一个计量系统的计数范围. 如时钟等. 计算机也可以看成一个计量机器, 它也有一个计量范围, 即都存在一个 "模".
例如:
时钟的计量范围是 1~12, 模 = 12. 表示 n 位的计算机计量范围是 0~2^(n) -1, 模 = 2^(n).
"模" 实质上是计量器产生 "溢出" 的量, 它的值在计量器上表示不出来, 计量器上只能表示出模的余数. 任何有模的计量器, 均可化减法为加法运算.
在计算机中, 计算加法要比计算减法要容易和便宜, 那么到底如何在计量装置中用加法代替减法呢?
这里我们可以用时钟举一个例子:
这是一个只有时针的时钟, 目前它指向 9. 如果我们想让他回退到 4. 我们有什么办法呢?
最简单的方法是, 倒退五个单位, 9-5=4. 但是这里我们不想让指针回退, 是否有另外的方法能够让指针指向 4 呢?
答案是把指针向前拨 7 个单位.
为什么能够这么做呢, 这就是前面我们提到的 "模" 的作用.
9+7=16=12+4, 在时钟这个计量机器上进位并不能显示出来, 所以最后时钟上显示的是 4.
在以 12 模的系统中, 加 7 和减 5 效果是一样的, 因此凡是减 5 运算, 都可以用加 7 来代替. 对 "模" 而言, 8 和 4 互为补数. 实际上以 12 模的系统中, 11 和 1,10 和 2,9 和 3,8 和 4,6 和 6 都有这个特性. 共同的特点是两者相加等于模.
同样的概念在计算机中也完全成立, 考虑一个四位的二进制数, 它的范围为[0,255], 模为 256, 把这个概念扩展一下. w 位的二进制数的模就为 2w.
模就是补码之所以能够进行二进制计算的基本原理.
补码
现在我们来谈谈本文的主角补码(Two's complement), 它的英文名依旧很迷惑, 不过没关系, 下面我会用引入模的概念来帮助我们理解补码的具体含义.
我们知道一个模为 10 的计算系统中,-4 与 + 6 的结果是相同的(9+6=10+5=15, 舍去进位最后的结果为 5 与 9-4 相同), 我们把这个概念扩展:
在一个模为 M 的系统中, 我们需要计算 m-n, 而 - n 在模为 M 的系统中可以转化为 +(M-n), 所以只需要计算 m+(M-n), 这样就成功的把减法转换成了加法.
把其扩展到二进制中:
在一个位数为 w 的计算系统中, 我们需要计算 m-n(n 为正整数), 而 - n 在系统中可以转化为 (2w-n), 所以只需要计算 m+(2w-n) 并把溢出的位舍去, 而 - n 的补码计算方法就是为 2w-n. 这也是英文名为 Two's complement 的原因, 意为 2 的补, 这里只有一个 2, 所以用 Two's complement.
补码似乎已经十分完美了, 它不仅解决了 0 的编码重复问题 (在补码中 0 只有[0000] 一种表示), 也成功的把减法变成了加法, 但是问题来了, 为了把减法变成加法而引入了减法不是像一个笑话吗(手动狗头).
这个时候我们就需要用到原码和反码了, 我们来仔细观察一下反码的计算方法, 对一个负整数位数为 w 的二进制数 - n 来说, 它的反码表示为(2w-1-n), 对比补码的表示 2w-n, 我们能够发现补码 = 反码 + 1.
我们很快就能推出对于一个负整数来说, 我们可以算出它的二进制相反数表示, 然后对所有的位取反, 最后加一, 得到的就是补码的值. 这样我们就使用反码成功解决了减法的问题.
当然了, 和反码一样, 上面的表示都是对负整数而言, 正整数的无符号码, 原码, 反码, 补码都是一样的~
CSAPP 给出了补码的数学公式:
同样的这个公式我们也可以通过证明得到, 并且通过这个公式我们也可以直接计算出补码的实际值.
由此我们能推断出由此我们能推断出 TMAXw=2w-1-1,TMINw=-2w-1. 如八位二进制原码的范围应该为[-128,127]. 这也是为什么在一般的程序设计语言中, 无符号数据类型的负数范围要比正数大了.
补码的计算就十分简单了, 直接相加就可以得到答案了~(当然溢出进位依然是要舍去的.), 这也是反码的计算原理.
一点感想
补码的出现解决了很多问题, 出于崇敬我上网搜索了补码的发明者, 结果是.... 没有搜索到, 并且经过查证, 早在 1645 年, Pascal 的计算器用的十进制补码. 后来到了 20 世纪, 经由冯诺依曼引入到了计算机中.
经典的理论与知识总是在流传中愈加让人感到先辈们的伟大, 想必 3 个世纪之前的那位大师在使用补码来解决计算问题时并没有想到有一天信息时代的崛起会让他的发现即使到了今天也依旧熠熠生辉.
来源: https://www.cnblogs.com/z-y-k/p/11800932.html