深入理解计算机系统:信息的处理和表示(二)整数四则运算

参考自:http://csapp.cs.cmu.edu/

开篇说明一下,本文不是介绍四则运算的具体执行过程,想了解具体过程的孩子们自己去看看计算机组成。

好了,话不多说。

1.  加减法

加法和减法没有区别,以下内容专注于加法。

1.1  无符号数加法

无符号数加法会出现溢出问题,当发生溢出的时候直接扔掉溢出进位。比如1111 + 0001 = 10000 直接扔掉进位1,结果为0000。考虑到溢出之后,我们可以总结出这样的计算式:

image

image

那么我们怎么来判断溢出呢?

对于s= x+ y; 当s <x 或者 s < y的时候就是溢出了。原因在于溢出的时候s=x+y-2w。而x,y都是小于2w的所以s<x ,s<y。

于是,我们可以写出函数来提前判断两个无符号数想加是否会溢出:

/* Determine whether arguments can be added without overflow */
int uadd_ok(unsigned x, unsigned y){
    unsigned  temp= x+y;
    return temp>=x;
}

下面给出一张图,全面展示四位无符号二进制树之间的想加情况:

image

好了,之前提到加法和减法没什么区别,那么我们怎么算无符号数减法呢?要知道无符号数可是没有“相反数”这个东西的。

为了解决这个问题,我们引入一个 加法逆元(additive inverse) 的概念。假设x的加法逆元是y,当且仅当(x+y)%2w=0,其中x,y为w位的二进制数。

加法逆元的计算公式:

image

以后计算减法的时候,就将被减数转换为其加法逆元,这样再相加就好了。

1.2  有符号数加法

有符号加法和无符号加法产生的二进制是一样的,它也会产生溢出,由于符号的原因它有两种溢出 正溢出和负溢出。正溢出是指两个两个正数想家结果为负数,比如0111 + 0001 =1000。负溢出就是两个负数相加为正数1001 + 1001 =0010。计算公式如下:

image

image

给出四位二进制有符号数相加的全部情况图:

image那么怎么判断是否溢出呢?

原理就是 负负得正,正正得负 就是溢出啦

/* Determine whether arguments can be added without overflow */
int tadd_ok(int x, int y) {
    int sum = x+y;    
    int neg_over = x < 0 && y < 0 && sum >= 0;
    int pos_over = x >= 0 && y >= 0 && sum < 0;
    return !neg_over && !pos_over;
}

当然了,如果有人神经叨叨的给你看他的实现方法:

/* Determine whether arguments can be added without overflow */
/* WARNING: This code is buggy. */
int tadd_ok(int x, int y) {
    int sum = x+y;
    return (sum-x == y) && (sum-y == x);
}

这段代码毫无疑问是错误的,为什么呢?因为我们知道加法是符合交换律的,那么(x+y)-x = (x-x)+y,也就是说上述判断条件始终成立。不信你可以验证。

说完加法,我们来看看怎么将减法转换为加法。当然是取相反数了,关键是,怎么取相反数?这是一个很有学问的地方,技巧妙的一谈糊涂。

x相反数是y,当且仅当x+y=0。那么我们显然得到y=-x这个公式了,但是要是有人问你最小负数的相反数是多少,你怎么办?举个例子,对于8位二进制而言,最小负数-128,-(-128)=128>127(8位二进制最大正数)。

哈哈,怎么办,怎么办。

结论是最小负数的相反数就是它自己!!你可以算一算1000 0000 + 1000 0000 =0000 0000 符合定义!

于是就有我们的求负数的公式了:

image

哈哈,看完这个我们再来看看怎么验证两个数相减会不会溢出,先看看下述代码:

/* Determine whether arguments can be subtracted without overflow */
/* WARNING: This code is buggy. */
int tsub_ok(int x, int y) {
    return tadd_ok(x, -y);
}

这个对不对呢?

应该是不对的,因为Tmin的相反数还是他自己,当x为正数,y为Tmin的时候,这个函数是会报错的,但是实际上是不会溢出的。

下面接着来说怎么实际操作来取相反数:一种方法是 各位取反后加一

image

第二种方法:我们仔细看看这个我们能看出一个规律,就是说我们沿着二进制从右往左走,知道遇到第一个1,然后将第一个1之后的每个位取反就好了。

2.  乘法

关于具体怎么做乘法自己去查阅计算机组成原理课本。我们先来看具体的二进制乘法的例子:

image

我们发现一个现象,就是说有符号二进制乘法和无符号二进制乘法,他们获得的最终结果的二进制表示是一样的!!、

好吧,处于严谨考虑,我们来证明一下下:

image

那么我们怎么来判断乘法会不会发生溢出呢?我们注意到,上面那个图中的乘法都是溢出的。

先给出代码,大家可以看看这个代码对不对?

/* Determine whether arguments can be multiplied without overflow */
int tmult_ok(int x, int y) {
    int p = x*y;
    /* Either x is zero, or dividing p by x gives y */
    return !x || p/x == y;
}

好吧,我承认我顽皮了,因为大部分同学(包括曾经的我)根本看不懂这个是什么东东!脑子里就俩个字:尼玛!

我们从理论上证明一下下:

首先我们知道对于w位的x和y,x*y是可以用2w位二进制来表示的,我们将这2w位二进制拆为两部分,高w位(用v来表示)和低w位(用u来表示),于是我们得到x*y=v*2w + u. u代表了代码中p的二进制表示,根据无符号数转换为有符号数的公式我们得到:u = p + p w−1 2w,于是我们可以将x * y = p + t * 2w. 根据溢出的定义,该乘法发生溢出当且仅当 t != 0。

其次,当x!=0的时候我们可以将p表示为p = x * q + r, |r| < |x|。于是我们有x * y = x * q + r + t * 2w。我们需要推导出t == 0的等价条件。当t = 0的时候,我们有x * y = x * q + r,而|r| < |x|,所以必有q=y;反过来,q=y的时候我们也可以推出t=0。也即是说t=0这个条件等价于q=y这个条件(前提是x不为0)。

q=p/x;

于是我们就得到了我们代码里面的判断条件: !x || p/x == y

那么如果我们不允许使用除法,但是允许你使用long long这个数据类型,你怎么来做乘法的溢出检查呢?

哈哈,其实这个就简单了。

/* Determine whether arguments can be multiplied without overflow */
 int tmult_ok(int x, int y) {
     /* Compute product without overflow */
     long long pll = (long long) x*y;
     /* See if casting to int preserves value */
     return pll == (int) pll;
 }

哈哈,说完这些之后我们来看看怎么将乘法转换为 移位和加法操作, 因为这两种操作时非常快速的。

我们来看,怎么将通过移位的方法一个数扩大两倍?很显然是左移一位。扩大四倍呢?左移两位。

其实我们就类比十进制乘法  列竖的计算式子就可以了。

这里面有个可以被优化的技巧,我们来看。加入被乘数是[(0 . . . 0)(1 . . . 1)(0 . . . 0) ].那么我们还要一个个的移位相加么?其中连续的1,起于从右到左的第m位,终于第n位。

我们可以这样,(x<<n+1) - (x<<m)。

好了,乘法也就这么多了,下面是除法。

3.  除法

除法这一块我们只涉及除以一个2k次方。对于正数以及无符号数而言,这意味着右移k位,高位补0。下图中斜体的0都是后来补的0。

image

而对于负数而言,我们需要采用逻辑右移,也就是说高位补1。例子见下图:

image

但是我们在图中发现一个问题,就是结果可能跟真实的值相差一,比如-12340/8应该为-48而不是-49,-7/2=-3而不是-4。怎么处理这个问题呢?

我们可以对负数做一点预处理,使得x=x+y-1。其中y就是我们的除数。

我们先来看看这么做对不对,(-7+2-1)/2 = (-6/2) =-3 没有问题啊。

证明如下:假设x = qy + r,其中0 ≤r <y。那么,(x + y − 1)/y = q + (r + y − 1)/y。当r=0的时候x/y=q,否则x/y=q+1。符合要求!!

代码实现是这样的:(x<0 ? x+(1<<k)-1 : x) >> k

哈哈,妙吧!

那么如果仅仅允许使用移位和&操作以及加法,怎么代码实现除法呢?(注意除数为2k)

int div16(int x) {
/* Compute bias to be either 0 (x >= 0) or 15 (x < 0) */
    int bias = (x >> 31) & 0xF;
    return (x + bias) >> 4;
}

看看这个代码写的!!亮瞎了吧。

当x为正数的时候,bias=0, 这没错。

当x为负数的时候,bias为15=16-1。又没问题!

好了,除法也就这些了!

4  总结练习

Assume we are running code on a 32-bit machine using two’s-complement arithmetic
for signed values. Right shifts are performed arithmetically for signed values
and logically for unsigned values. The variables are declared and initialized as
follows:
int x = foo(); /* Arbitrary value */
int y = bar(); /* Arbitrary value */
unsigned ux = x;
unsigned uy = y;
For each of the following C expressions, either (1) argue that it is true (evaluates
to 1) for all values of x and y, or (2) give values of x and y for which it is false
(evaluates to 0):
A. (x > 0) || (x-1 < 0)
B. (x & 7) != 7 || (x<<29 < 0)
C. (x * x) >= 0
D. x < 0 || -x <= 0
E. x > 0 || -x >= 0
F. x+y == uy+ux
G. x*~y + uy*ux == –x

解答如下:

A. (x > 0) || (x-1 < 0)
False. Let x be −2,147,483,648 (TMin32). We will then have x-1 equal to
2147483647 (TMax32).


B. (x & 7) != 7 || (x<<29 < 0)
True. If (x & 7) != 7 evaluates to 0, then we must have bit x2 equal to 1.
When shifted left by 29, this will become the sign bit.


C. (x * x) >= 0
False. When x is 65,535 (0xFFFF), x*x is −131,071 (0xFFFE0001).


D. x < 0 || -x <= 0
True. If x is nonnegative, then -x is nonpositive.


E. x > 0 || -x >= 0
False. Let x be −2,147,483,648 (TMin32). Then both x and -x are negative.

F. x+y == uy+ux
True. Two’s-complement and unsigned addition have the same bit-level behavior,
and they are commutative.


G. x*~y + uy*ux == -x
True. ~y equals -y-1. uy*ux equals x*y. Thus, the left hand side is equivalent
to x*-y-x+x*y.

原文地址:https://www.cnblogs.com/xubenben/p/3650819.html