zoukankan      html  css  js  c++  java
  • 再谈二进制补码

    写在前面:此文章不是介绍补码的基础知识,在您向下阅读之前,确保已经对数据的二进制表示有一定的了解,否则请勿浪费时间。

    在初学计算机组成原理 (ICS) 的时候,入门的第一课便是数据的编码表示,其中一个重要的部分就是补码 (Complement) 。

    在学习补码时,有个结论是经常提到的,而且要求我们记住:

    正数的补码是其二进制表示本身,负数的补码是对应正数补码的按位取反再加一。

    这个结论等价于:对于某个整型 (k)(k) 的相反数:-k = ~k + 1

    在大二学习 ICS 这门课的时候,我经常使用这个结论去写出负数的补码(先写正数的补码,然后按位取反再加一)。

    但同时我有一个「坏习惯」,当我想把某个负数的补码转换为 10 进制表示时,我总是先把补码按位取反再加一,然后求出正数,即可求得负数。

    在学习 CMU 的 15213-CSAPP 的 Course 时,从另外一种角度来理解补码似乎更符合我的思维习惯。

    (X=x_{w-1} ... {x_0}) 是一个比特串,如果将 (X) 视作是一个无符号数 (unsigned) ,那么有:

    [B2U(X) = sum_{i=0}^{w-1}x_i cdot 2^i ]

    如果将 (X) 视作是一个有符号数 (signed) ,那么有:

    [B2T(X)= -x_{w-1} cdot 2^{w-1} + sum_{i=0}^{w-2} x_i cdot 2^i ]

    下面是这个公式给我带来的几点启示。

    特殊值

    对于 32 位的 int ,我们知道 int x = 0x80000000 表示的是十进制中的 -2147483648

    大一上 C 语言的时候,老师当时的解释:最高位表示的是符号位,因此按「道理」来说, 0x00000000 表示 +00x80000000 表示 -0 ,但是 0 只需要一种编码表示就够了,所以把 0x80000000 这个负数的编码「分配」给 -2147483648 。同时也是为了满足计算机的「模运算」系统(大溢出变小,小溢出变大),即:

    • 0x80000000 - 1 = 0x7fffffff
    • 0x7fffffff + 1 = 0x80000000

    这种解释对我来说总感觉难以接受,很别扭的感觉。

    但是采用上述公式 (B2T(X)) 就很好地把所有的二进制编码 统一 映射到我们的十进制数了,整型范围内无一例外。

    补码转十进制

    假设有 ((10011)_2) 这个比特串,将其视作有符号数解释。原来没有这个公式,不容易看出对应的十进制数值。原来我是这么转换的:

    k    = 10011
    ~k   = 01100
    ~k+1 = 01101 = 13
    

    可得 (k=-13)

    现在有了上述公式,就可以口算:-16 + 3 = -13 。至少对我说,这个效率的提升是显然的(当 (k) 有 32 位的时候更加明显)。

    补码比较大小

    (a=(10011)_{2}) , (b=(10101)_{2}) ,将其均视作有符号数解释。原来我的做法是先转换为十进制,然后再比较大小。但根据上述公式 (B2T(X)) ,可以看出,公式的 (x_{w-1} cdot 2^{w-1} = -16) 部分是相等的,所以只需比较后半部分,而 0011 < 0101,因此显然可以得出 (a < b)

    这就省略了转换十进制的繁琐过程。

    符号扩展和位截断

    符号扩展,在之前我有一点难以理解。比如:

    int8_t x0 = -8;		// 1111 1000     
    int16_t x1 = x0;	// 1111 1111 1111 1000
    

    显然,x1 的值也是 -8 ,但是我一直很迷惑这其中的原理,怎么证明符号扩展(整数扩展补 0 ,负数扩展补 1)后的值不变?对正数来说是显然的;对于负数,我们可以从其绝对值考虑 abs(k) = ~k + 1,所以只需要证明 ~k 的值不变即可,这也显然成立。

    下面尝试用上述公式证明。设比特串 (X_1 = x_{n_{1}-1} ... x_0),现在将 (X_1) 扩展为 (n_2) 位长的有符号数 (X_2)

    Index (n_2 - 1) ... (n_1 - 1) ... 0
    (X_1) (null) (...null...) (1) (x_{n_1-2} ... x_1) (x_0)
    (X_2) (1) (...111...) (1) (x_{n_1-2} ... x_1) (x_0)

    对于 (X_1)

    [B2T(X_1) = -2^{n_1-1} + sum_{i=0}^{n_1-2}x_i cdot 2^i ]

    对于 (X_2)

    [egin{align} B2T(X_2) &= -2^{n_2-1} + sum_{i=0}^{n_2-2}x_i cdot 2^i \ &= -2^{n_2-1} + sum_{i=n_1-1}^{n_2-2}2^i + sum_{i=0}^{n_1-2}x_i cdot 2^i \ &= -2^{n_2-1} + frac{2^{n_1-1}(1-2^{n_2-n_1})}{1-2} + sum_{i=0}^{n_1-2}x_i cdot 2^i \ &= -2^{n_1-1} + sum_{i=0}^{n_1-2}x_i cdot 2^i \ &= B2T(X_1) end{align} ]

    证毕。

    对于上述 2 种证明方法,第一种显然更简单,但个人更喜欢第二种,更直观,更暴力(●=◡=●)。

    有点可惜,自己本科没有上过关于编码的理论课程,但我觉得最初搞计算机,设计出「补码」这种编码方式的那些人实在太强大了。「补码」不仅统一了计算机中加减法的运算,而且对于 C 语言中符号扩展,位截断这些处理都变得十分简单。

    对于「位截断」来说:

    int16_t a = -99;
    int8_t b = a;
    

    证明 b 的值仍然是 -99 的过程就是上述证明的逆过程。

    补码的四则运算

    加减法

    在计算机的硬件层面,是没有实现减法的,在 ALU 中有一个叫加法器 Adder 的东西,加法和减法运算都是由它来完成。对于硬件而言,这些家伙没有十进制的概念,也没有「符号位」的概念,给它 2 个的比特串,他就老老实实的算加法:

       1111 1111
    +  0000 0001
    ------------
     1 0000 0000
    

    对于加法器而言,0xff + 0x01 的结果就是 0x00 ,它可不管你给的有符号数还是无符号数(这些事情都应该是程序员自己来管的,所有 CSAPP 全称叫 Computer System, A Programmer's Perspective )。溢出的 1 bit 在汇编层面我们可以通过 EFLAGS 来获取,但是这些在 C 的层面是「不可见的」。

    那么加法器如何计算减法呢?答案是:x - y = x + ~y + 1

    乘除法

    乘法指令 mul 的指令周期大约是 3 ,除法指令 div 的周期大约是 30 (这一点 CSAPP-2015 的 Course 有提到)。因此对于整数的乘除,值得研讨的是通过移位来优化它们(这是编译器层面的工作)。

    对于乘法 a * 10 而言,10 的二进制是 1010 ,在第 1 位和第 3 位分别有 1 ,因此 a * 10 = a << 1 + a << 3 。这一点通过文章开头的 (BTU(X)) 公式是很容易证明的。无论 a 是正数还是负数均适用。

    对于除法 a / k ,只有 k 是 2 的幂次形式时才能进行移位优化,我们设 (k = 2^n)

    • 如果 a 是正数,a / k = a >> n
    • 如果 a 是负数,(a / k = (a+2^n-1)>>k)

    为什么负数这么特殊?举个 8 位 int 的例子:

    int8_t a = -3;  // a = 1111 1101 
    a / 2 should be -1.
        
    a >> 1 = (1111 1101) >> 1 = (1111 1110) = -2
    (a + 2^n - 1) >> 1 = (-3 + 1) >> 1 = (-2) >> 1 = (1111 11110) >> 1 = (1111 1111) = -1
    

    因为对于负数, a >> n 丢弃了某些 1 ,使得 a / k 真实数值向数轴的负方向移动。因此移动 n 位,需要加上 (n) 个 1 进行修正,该数值就是 (2^n - 1)

    对于正数来说,a >> n 丢弃的某些 1 ,恰好使得 a / k 向数轴左边移动,这正好就是我们需要的「丢弃」小数位的除法。

  • 相关阅读:
    今日小结 5.7
    今日小结 5.2
    今日小结 4.30
    今日小结 4.29
    设计模式 笔记1
    第一次找实习
    Java入门 任务表
    今日小结 4.24
    今日小结 4.18
    今日小结 4.17
  • 原文地址:https://www.cnblogs.com/sinkinben/p/12377232.html
Copyright © 2011-2022 走看看