在刚开始接触编程的时候,可能会遇到两个正数相加之后得到一个负数这种奇怪的事情,以及浮点数的比较表达式 x < y 和 x - y < 0 竟然得到不同的结果。这些其实都是计算机运算的有限性造成的,理解计算机计算的有限性有助于我们编写更加可靠的程序。
整数运算
考虑到两个非负整数 x 和 y,满足 ,也就是说每个数都可以表示为 w 位的无符号数字。但若把它们相加,则它们的结果满足
,也就是说它们的结果可能需要 w+1 位才能表示的下。
更一般的,两个 w 位的数字相加其结果可能需要 w+1 位来表示,两个 w+1 位的数字相加其结果可能需要 w+2 位来表示,以此类推,这种持续的“字长膨胀”意味着要想完整的表示算术运算的结果,我们不能对数字的字长做任何限制。像 python 这种脚本语言实际上就支持无限精度(实际上还是有机器的内存限制)的运算。但更多的语言都只支持固定精度的运算。
无符号加法和乘法
对任意满足 的无符号数 x,y 有:
可以看到,正常情况下 x+y 的值保持不变,而溢出的情况则减去 2的结果。原因在于两个 w 位的数字相加,最大的结果就是需要 w+1 位来表示,且最高有效位一定是 1。丢弃它就相当于是从结果中减去 2 。而对于正常非溢出的情况,和的 w+1 位相当于等于 0,丢弃它并不会改变这个值。这可以视为一种形式的模运算,对 x+y 的位级表示,丢弃任何权重大于 2的位就可以计算出和模 2。
Rust 程序对于发生溢出的时候会直接 panic 掉,C/C++ 程序则不会将溢出作为错误信号发出。但有时候我们可能需要检测程序是否发生了溢出,根据上面的公式,无符号数加法中检测溢出的方式也很简单,就是判断 x+y 的和是否小于 x 或 y。
对应的,对于两个 w 位的无符号做乘法,其结果可能需要 2w 位来表示,将去截断为 w 位的表示等价于计算该值模 2的值。有:x*y = (x * y) mod 2```` 。
补码加法和乘法
对满足 的整数 x,y 有:
同样的,对于补码的加法也是把最后的结果截断到 w 位来表示,对于正常的情况,截断本就不存在的 w+1 位不会有任何影响,对于结果小于最小值的情况,也就是负溢出的情况,截断最高的 w+1 位这个负权重后就相当于最后的结果加上 2,而对于结果大于最大值的情况,也就是正溢出的情况,和无符号数的加法一致。实际上大多数计算机使用同样的机器指令来执行无符号数加法。
notes: 求补码的非的一种方式就是每一位取反之后再加 1。
_
对于补码的乘法,其实就是相当于计算该值模 2 ,再把无符号数转换成补码来实现的。
乘以常数
在大多数机器上,整数乘法指令相比加法,减法,位级运算,移位等操作都要慢 3~10 甚至更多的时钟周期。所以编译器基本都会使用位移和加法的运算组合来代替乘以常数因子的乘法操作。
先考虑乘以 2 的幂的情况,设 x 的位模式 表示的无符号整数,那么对于任意
,都有
的 w+k 位的位模式表示为
,也就是右边增加了 k 个 0。也就是说对一个数左移 k 位,相当于对它乘以 2。
对于固定长度的字长左移 k 位,其高 k 位被丢弃,得到: 也是适用的。
比如说有一个数 5,乘于 4 之后得到 20,用位运算来表示就是 5 << 2 = [ 0000 0101 ] << 2 = [ 0001 0100 ] = 20。那如果说对于一般的非 2 的幂的常数又当如何呢?其实只要把它转换乘多个 2 的幂和即可,比如 5 = 4 + 1 =
2 + 2 ,那么就有:
这样转换之后,不用做任何乘法就能计算出任意 的结果。
