zoukankan      html  css  js  c++  java
  • 深入理解计算机系统:信息的处理和表示(二)整数四则运算

    参考自: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.

  • 相关阅读:
    微服务实战系列(七)-网关springcloud gateway
    微服务实战系列(六)-网关springcloud zuul
    微服务实战系列(五)-注册中心Eureka与nacos区别
    微服务实战系列(四)-注册中心springcloud alibaba nacos
    中国唯一入选 Forrester 领导者象限,阿里云 Serverless 全球领先
    面对不可避免的故障,我们造了一个“上帝视角”的控制台
    实习就参与“服务过亿用户的项目”,是什么体验?
    Go Mysql Driver 集成 Seata-Golang 解决分布式事务问题
    云原生时代下,容器安全的“四个挑战”和“两个关键”
    OpenKruise v0.8.0 核心能力解读:管理 Sidecar 容器的利器
  • 原文地址:https://www.cnblogs.com/xubenben/p/3650819.html
Copyright © 2011-2022 走看看