补码
程序设计课对补码一笔带过,然而我想,这样反直觉的东西是需要细细思考和理解的,翻阅网上的文件,都不能令我满意,于是作此文,探讨补码的本质。本文从零开始,不作任何先验的假设
1. 如何表示无符号整数?
我们知道,用二进制形式表示无符号整数是简单,直观,自然的。但是,理解这种 “简单,直观,自然” 对接下来探讨有符号整数有很大的好处。
先来厘清自然语言带来的含混之处:
-
“无符号整数“ 是一个形如
的集合 ,这总是一个有限集, (当然,通过增加 ,实际上可以包括任意的整数,一个简单的极限过程) -
“二进制形式” 是由相同长度的的所有 "0-1" 串构成的集合
,若长度为 ,则 (暂时不使用“二进制数”的概念,因为本文的讨论先于它) -
“表示” 一词的确切含义是,能够构造两个集合之间的双射(当然,出于实际考虑,这个双射需要是可计算的,可计算这个条件非常容易满足,于是在下文忽略)
我们知道,两个有限集合存在双射,当且仅当它们的元素数量相同,那么只需要
不妨设
-
先确定 (0000) 表示 0,因为它们如此相像
-
然后,(0001) 表示 1,因为在熟悉的十进制中,(0001) 恰恰就是 1,
-
此时已经有了一种倾向:保持左边尽可能都是 0,秉持这一信念,就应当用 (0010) 或者 (0011) 来表示 2, 以使得最左边只有 2 个 0,由于前者看起来“更小”,于是我们选择 (0010),而把 (0011) 留给 3 ......
-
最后,只剩下了 (1111) 与 15,配为一对
事实上,我们所有人正是使用这种方法表示无符号整数的,但要注意到,存在其他不同的可能,例如,用 (0000) 表示 15,(0001) 表示 14 ...... 最一般的是,我们随机确定
一般地,用
更严格地说,设
凭借已经学习的知识,我们知道,”
但正如前文所说,在此使用进制的观点是理解上的循环,违背了从零开始设计的原则,所以,让我们重新理解为什么 **"容易定义
实际上,这是因为
因此,只需保证最右边的等式恒成立!由于我们正是按照后继的顺序为自然数
我们仍然以
至此,我们已经理解了为何这种司空见惯的表示方法恰恰具有关于后继运算的同构性以及容易计算性(考虑自由度,可知这描述的是一件事,而不是两件事),可为什么这一性质就是尤其重要的?
我们先暂且把计算机理解成只用于处理外部整数的机器,它接收整数输入,转换为二进制形式存储和运算,输出整数。那么我们最关心的一点就是二进制运算的速度。
熟知,根据皮亚诺公理,自然数的所有其他运算都由后继运算定义,这些运算实际上的计算方式也和后继息息相关,所有其他运算都由后继运算定义 ,这是至关重要的,因为这说明后继运算上的同构能够保持到所有其他运算上,于是我们为了说明某种运算仍然保持同构,只需要说明
一方面,这以坚实的理论基础揭示了为什么后继运算尤为重要,特别地,为什么后继运算的计算速度尤为重要;另一方面,我们在下文只需要说明
2. 如何表示有符号整数?
本文几乎一切理论基础几乎都在第一节建立起来了,因此原先复杂的补码也将变得容易理解,收获成果的时间到了!
如果参照第一节,现在应当给出对名词的定义,但这里并不一样。称有符号整数是集合
我们考虑有符号整数的功能性,这是另一个先验性的假设,可以想想,一个包含很多正数,很少负数的集合是没有意义的,因为最基本的求相反数运算都不能定义,这说明
有了第一节的经验,我们现在有明确的目标:寻找
显然,
-
先确定 (00000) 表示 -16
-
然后,(00001) 表示 -15
-
...... 由 (01111) 表示 -1,(10000) 表示 0,(10001) 表示 1 ......
-
最后,剩下了 (11111) 与 15,配为一对
于是,这个问题似乎就解决了,因为这种映射同样保持了简单的后继运算!
是的,如果计算机确实是只用于处理外部整数的机器,并且所有需要处理的数都在
-
无论如何,计算机需要处理各种不同大小的数,特别地,需要处理不同值域的数据,如果采用以上的映射方式,则对于不同的
,映射关系完全不同,这意味着如果要在 与 之间进行类型转换会比较困难 -
熟知,减法作为加法的逆运算而存在,为了计算减法,必须先对减数进行求逆操作,设想对十进制中的 5 求逆,只需要加一个负号成为 -5 即可,这种对称性没有被保持到二进制中,所以重要的求逆操作计算量更大
由此,我们得到了又一个先验假设:映射应当具有稳定性和对称性:
- 稳定性:类型转换容易
- 对称性:对加法求逆容易
我们在后继这个一元运算中已经获得了一个重要经验:某个一元运算容易,意味着运算的输入与输出相似或相近,首先,我们希望保持 “0-1” 串的后继运算规则不变,这样就不必重新担心它,其次,要寻找一种新的映射方式满足稳定性和对称性,即:类型转换和求逆前后的 “0-1” 串相似或相近!
事情变得有趣了起来,在第一节,我们先确定了集合
现在情况则相反了!我们确定
也就是说,我们按照后继顺序依次为
不失一般性,以下设
-
先成环再对齐,这个视角首先能够为如何满足稳定性带来一些启发:我们把
中的 0 与 中的 (00000) 对齐,这样可以保证 和 下的 0 都以全 0 串表示,这必然为类型转换带来很大的方便 -
现在已经规定了两个环如何对齐,而
成环的方式确定,那么可变的只有 成环的方式,我们知道,一共有 种不同的成环方式(圆排列),但其中只有很小一部分是备选项。为了使 与 的映射本身容易计算,在不考虑边界时,我们仍然希望 0 的后继是 1,1 的后继是 2 而不是 5,至于 -2 的后继 是 -1 还是 -3 则留给后续讨论 -
现在情况缩减到了几种?所有非负数内部连续连接,只有一种方向,所有负数内部连续连接,有两种方向,我们把这两条子链拼接起来,实际上只剩下了两种可能!!
-
第一种是:
- 第二种是:
-
对于两种情况,分别绘制成环,与环
对齐,观察对称性,容易知道第二种才是最优的选择,因为此时所有正数都与负数在环上关于 0 对称! -
于是,这一种对应方式就是现今大部分计算机存储有符号整数的方式,也就是补码!!!
3. 补码的定义
恭喜! 来到本节时,我们已经从零开始,共同重新发明了补码!整个过程都是十分自然的,实际上,稍好的直觉完全可以使你跳过几步,来到终点。因此,补码并非什么天才的创造,而是自然的相反。遗憾的是,很大一部分人,包括我自己,是这样初次接触它的:
非负数的补码是原码本身,负数的补码是原码的非符号位取反再加一
总之,在这一节,我们将把上文推导得到的补码定义与其他定义和性质结合起来。
视起点为环
现从 -15 归纳至 -1,证明 “负数的补码是原码的非符号位取反再加一”,记号与第二节相同,另记
注意到此时环
关于二进制形式求逆的实际计算方式,可观看 Nand2Tetris-负数