之前只听说过原码、反码、补码,最近刷题才学到还有一个移码,学习过之后觉得也非常有意思,于是记录一下。
从补码说起
移码也叫增码或偏置码,和补码一样,也是为了表示有符号整数而搞出来的一种编码方式,比如8位比特,如果是无符号数那么可以表示[0,255]这256个正数,如果要考虑符号的话,符号需要占一位,那么能表示的数的绝对值就会少掉一半,即128个了,最简单的原码和反码,由于+0和-0不一样,导致表示范围只能有[-127,127],且加减法需要特殊处理,因此被抛弃了,补码的伟大之处就在于,0有唯一的表示方式,且加减法可以统一处理,如果不了解原理的可以看我的这篇文章:有符号数的加减法原理分析,因此现代计算机表示整数的时候基本都采用补码。
IEEE 754
那为什么还要有移码呢?首先需要复习一下浮点数的表示方法,即IEEE 754表示法,这里就不详细介绍了,大致就是一个浮点数的二进制表示由三个部分组成:符号位(Sign, S)、指数位(Exponent, E)、尾数位(Mantissa, M),对应的值就是 ( − 1 ) S × M × 2 E (-1)^S \times M\times2^E (−1)S×M×2E,如果 E ≥ 0 E\geq0 E≥0说明该浮点数大于等于1, E < 0 E<0 E<0就说明小于1,也就是说E是个有符号整数,所以应该采用某种编码方式,但是如果用补码的话存在两个问题:1. 比较大小和作差不方便,负数第一位是1,正数是0,这样看起来负数比正数大了,算出两者之间的差还需要先把小的那个取反加一(做补码)再相加,但是浮点数的加法第一步就是对阶,找出指数的大小关系并让小的对齐大的。2. 用补码表示阶码的时候,随着阶码不断减小,从 − 1 10 -1_{10} −110到 − 12 8 10 -128_{10} −12810,也就是 1111111 1 2 11111111_2 111111112到 1000000 0 2 10000000_2 100000002,当阶码无限小,产生了下溢的时候,阶码反而变成了一个0开头的正数,而且从 0 10 0_{10} 010到 − 1 10 -1_{10} −110是从 0000000 0 2 00000000_2 000000002突然变成了 1111111 1 2 11111111_2 111111112,这样就非常的不平滑。其实这两点的根本原因都在于,二进制表示的相对大小顺序和实际的值不一致。
因此就提出了移码,对指数进行偏差修正,即加上一个bias,将它的值调整到一个无符号数的范围内以便进行比较。此外,指数采用这种方法表示的优点还在于使得浮点数的正规形式和非正规形式之间有了一个平滑的转变。
正规形式和非正规形式的区别如下(以32位为例,E有8位,M有23位,bias=127(64位的话bias=1023))
- 当E=0时,表示指数产生了下溢,为非正规形式,此时值为 ( − 1 ) S × 0. M × 2 − 126 (-1)^S \times 0.M\times2^{-126} (−1)S×0.M×2−126,特殊情况为当M=0时表示0.
- 当0 < E < 255时,正规形式,值为 ( − 1 ) S × 1. M × 2 E − 127 (-1)^S \times 1.M\times2^{E-127} (−1)S×1.M×2E−127,即小数点前的数为1,这就是典型的科学计数法。
- 当E=255时,M为0表示∞,M不为0表示NaN。
为什么要分正规和非正规呢?试想,如果都是按照正规形式来,当M和E都为0的时候值为 1.0 × 2 − 127 1.0\times2^{-127} 1.0×2−127,虽然很小但是不是0,这种表示方式根本无法表示0!那如果我强行规定M和E都为0值就为0呢?那么再想一下,此时是绝对值第二小的是 1.0...1 × 2 − 127 1.0...1\times2^{-127} 1.0...1×2−127,中间省略21个0,它与绝对值第三小的值的差距为 2 − 23 × 2 − 127 = 2 − 150 2^{-23}\times2^{-127}=2^{-150} 2−23×2−127=2−150,而与0的距离为 1.0...1 × 2 − 127 1.0...1\times2^{-127} 1.0...1×2−127,比 2 − 150 2^{-150} 2−150多了 2 − 27 2^{-27} 2−27次方,是它的123倍!!这就是所谓的的突然式下溢出(abrupt underflow)问题,可以说是非常突然的下溢出到0。这种情况的一种糟糕后果是:两个不等的小浮点数X与Y相减,结果将是0。而采用了非正规,就可以使从 1.0 × 2 − 126 1.0\times2^{-126} 1.0×2−126到0的过度更加平滑,从1.0…0过渡到0.1…1再到0.00…01再到0.0…0,距离都是 2 − 23 × 2 − 126 = 2 − 149 2^{-23}\times2^{-126}=2^{-149} 2−23×2−126=2−149。
脱离浮点数再看移码
其实如果只搜移码这个概念,会发现bias一般取 2 n − 1 2^{n-1} 2n−1,而不是上面看到的 2 n − 1 − 1 2^{n-1}-1 2n−1−1,再继续观察可以发现,补码和移码虽然得到的过程完全不同,但是结果只差了一个符号位!其余位都是一样的,因为其实你看,一个负数的补码去除符号位的剩余部分,和它的绝对值相加,就是 2 n − 1 2^{n-1} 2n−1,因为得到补码的过程是对绝对值的无符号表示取反加一,取反意味着两者相加为 2 n − 1 − 1 2^{n-1}-1 2n−1−1,即111……1111,加1不就是 2 n − 1 2^{n-1} 2n−1了吗!所以算移码的时候也可以用这个方法来算,或者说算补码的时候可以先用 2 n − 1 2^{n-1} 2n−1减去绝对值,再加上符号位。
不过这仅限于整数啦~小数的话没有移码,补码就只能通过取反加一来获得了。附一张各种编码方式的取值范围。