CSAPP学习笔记二 -- DataLab

Feb. 23th, 2017


CSAPP 系统级程序 位操作

Notes

  1. 长度为$w$的位向量表示集合 $\{0,\cdots,w-1\}$的子集,因此位操作等价于集合操作。

    data_1

  2. 为什么右移补最高位叫“算术右移”呢?因为在二进制补码的解释规则中,算术右移将原来的值除以2。与此相似的是“符号扩展”,补最高位的数不会改变原来的值。

  3. 位表示是不变的,变的只是对这些位的解释规则,有一张很直观的图说明了这一点:

    data_2

  4. 在比较操作中,如果同时存在有符号数和无符号数,那么会都当成无符号数来解释。(为啥呢? 不过都当成有符号数来解释也不太说得过去)

  5. 有符号数和无符号数的加法溢出,只可能会造成结果增大或减小$2^w$。

    data_3

  6. 为什么二进制补码表示里,一个数取的相反数等于这个数按位取反再加一呢($-x= \sim x+1$)?整理一下这个表达式就很好理解了:$x + \sim x= 11\cdots 1 = -1$,哈哈好厉害!

  7. 什么时候用无符号数呢?

    a. 在进行取模运算时

    b. 将位作为向量表示集合时

    c. 系统中用作位掩码、设备号等时

  8. 内存中的字长(word size)限制了寻址空间的大小,每一个字长的地址表示一个字节。由于地址本身存储也占用空间,而且还有硬件方面地址线数目的限制,所以存在一个字长和寻址空间大小的权衡:字越长,寻址空间越大,但地址本身也占据较大空间,地址线要增多。所以现在64位字长的机器最多支持$2^{40}$左右字节寻址,一方面物理内存没有那么大,另一方面虚拟空间与实际空间的比值过大会降低缓存命中率。

    还有一点,一个存储器中有多个存储体,CPU在对他们寻址时是同时获取多个存储体中的数据。比如说32位的地址总线可以一次性读取4字节的数据,这4个字节就作为1个字,因为这是CPU读取数据的最小单位,至于读取之后置于寄存器中使用其中的一个或多个字节就是另外一回事了。

    data_6

  9. 在C语言中,一些涉及到整数极限值的小问题:

    signed int x,y;
    unsigned int ux,uy;
    

    部分判断方便举例,假设整型数长为$4$。

    算式 T/F 原因
    $x<0 \to x\cdot 2<0$ F 1011*2=0110
    $ux\ge 0$ T 无符号数都非负
    $x\&7=7 \to (x<<30)<0$ T x最低3位为1,左移后最高位为1,为负
    $ux>-1$ F 比较时按无符号数解释,即$ux\le -1$
    $x>y \to -x<-y$ F 令y=TMin,那么-y=y,不可能比任何数大
    $x*x\ge 0$ F 0011*0011=1001<0
    $x>0 \&\& y>0 \to x+y>0$ F 0111+0001=1000<0
    $x>=0 \to -x<=0$ T 所有正数的相反数最高位均为1,0另考虑
    $x<=0 \to -x>=0$ F x=TMin,-x=x<0
    $(x$|$-x)>>31==-1$ F x=TMin,那么x|-x的最高位为0
    $ux>>3==ux/8$ T 无符号数进行逻辑右移
    $x>>3==x/8$ F 对于不能整除的负数,两者约化方向不同
    $x\&(x-1)!=0$ F x=TMin,x-1=TMax,两者与为0
  10. IEEE浮点数表示规则(部分):

    data_4

    这种表示方法中存在两个0,+0和-0。 还要注意一下这里的几个叫法,$V=(-1)^s\times M\times 2^E$中三部分分别叫符号、尾数和阶码;而位表示中三部分分别叫符号位、阶码字段和小数字段。

  11. 用IEEE标准来表示小数的优势在哪里?

    据说IEEE浮点表示法是一群数学家在一起制定的,所以对于数字位表示的舍入和溢出都有很简单的规则,但是硬件上并不好实现。

    对于小数来说,如果使用一般的方法来存储,即$(2.75)_{10}=(10.11)_2$。存在两个限制,一是只能准确表示形如$\frac{x}{2^k}$的数,而许多有理数的这种二进制表示是无限循环小数,如$(\frac{1}{3})_{10}=(0.[01])_2$;二是存储空间有限,这种方式无法存储很大或很小的数。

    考虑基于科学计数法来表示一个数,它解决了表示范围的问题。比如对于数$(8.55)_{10}$,如果使用上面的规则来存储,为$(1000.10[0011])_2$,假设用32位来表示这个数,其中小数点前的部分用$k$位表示,先不考虑负数,它能表示的范围是 $[0,2^k)$。如果使用科学计数法,有$8.55_{10}=(1.00010[0011]\times 2^3)_2$,同样使用$k$位表示小数点前的部分,可以表示的范围是$2^{[0,2^k)}$,要比之前大上许多。

    接下来要解决两个问题:

    1. 这种方式无法表示负数。
    2. 它无法表示$[0,1)$之间的数。

    对于前者,只需要加上一个符号位即可;对于后者,我们引入一个偏移量$bias$,将其置于科学计数法中2的次幂上,即$2^{[-bias,2^k-bias)}$,选取合理的$bias$值即可表示小于1的数。假如选取$bias=2^{k-1}$,那么可以发现这种方法能准确表示的$[0,1)$和$[1,\infty)$之间的数的数目是相同的。

  12. 浮点数转化为对应十进制数

    IEEE表示的三部分分别为符号位$s=0/1$,阶码字段$exp=e_{k-1}\cdots e_1e_0$,小数字段$frac=f_{n-1}\cdots f_1f_0$

    首先根据符号位$s$确定正负:$s=1$为负,$s=0$为正。

    再看$k$位阶码字段 $exp$

    1. 若为全1,再看$n$位小数字段 $frac$,若全为0,表示 $\infty$;若不全为0,表示$NaN$

    2. 若全为0,那么数$V=(-1)^s\times (0.f_{n-1}\cdots f_1f_0)_2 \times 2^{1-(2^{k-1}-1)}$

    3. 若非全0全1,那么数$V=(-1)^s\times (1.f_{n-1}\cdots f_1f_0)_2 \times 2^{exp-(2^{k-1}-1)}$

  13. 浮点数计算结果的舍入规则分为三种:

    1. 如果溢出部分大于0.5,那么向上舍入
    2. 如果溢出部分小于0.5,那么向下舍入
    3. 如果溢出部分等于0.5,那么使数的末位为0,即如果原来数末位为0,那么不变(向下舍入);如果为1,则加1(向上舍入)

    对于向偶数舍入,主要是考虑统计意义上的误差,我们认为在浮点运算中出现奇数和偶数是等可能的,比如说当进行若干个正数累加时,向上舍入和向下舍入都会增大统计意义上的偏差,而向偶数舍入误差较小。

  14. 在C语言中,一些涉及到浮点数溢出的小问题:

    int x;
    float f; //不为无穷或NaN
    double d; //不为无穷或NaN
    
    算式 T/F 原因
    $x==(int)(float)x$ F x=TMax,转换成float时进行了舍入,从$1.1\cdots 11\times 2^{30}$变成了$1\times 2^{31}$,再转换回来时x=TMin
    $x==(int)(double)x$ T double足够长,转化时不会丢失精度
    $f==(float)(double)f$ T double足够长,转化时不会丢失精度
    $d==(double)(float)d$ F 当d过大在float中会被解释为$\infty$
    $f==-(-f)$ T 浮点数取非只是对符号位取反
    $2/3==2/3.0$ T 前者为整数除法,舍弃小数部分,后者为浮点数除法
    $d<0.0 \to d*2<0.0$ T 浮点数乘法如果溢出不会影响符号位
    $d>f \to -f>-d$ T 浮点数取非只是对符号位取反,比较时先转换为double,所以也不会丢失精度
    $d*d>=0.0$ T 可能会溢出到$\infty$,但不会影响到符号位,所以一定非负
    $(d+f)-d==f$ F 当d远大于f时,左边的加法会舍入,再减d后得到0
  15. ASCII字符集,使用一个字节来表示字母数字符号等,低7位有效,第8位用于:

    1. 早期系统中用与保存其奇偶性
    2. 打印机中为1表示打印图形,为0表示打印字符
    3. PC中置1表示选择扩展的ASCII字符集,打印希腊字母、算术字符等,随打印机而不同
  16. 整数和浮点数的加减乘除(待续...)

Little Trick

  1. 引用指针的时候,为了避免指针为空的情况,在条件判断时可以用逻辑与运算的短路原则,即 (p && *p)。

  2. 在除2的$k$次幂的运算(即算术右移 $k$位)中,如果原数为负,导致在约化的时候并非向$0$的方向,而是向$-\infty$的方向。因此对原数先加上$2^k-1$,正好可以达到两个效果:对于原本就能整除的数,相当于将结果原本全为0的小数位变成全为1,最后仍然扔掉,不影响结果;如果小数点后并非全为0,那么会溢出到整数位,相当于将结果加一,正好可以满足向$0$约化的要求。这个方法也可以用在别的约化相关的问题上。

Attention

  1. 长度为$w$的二进制数在位移操作时位移范围是 $0\le s<w$。在C语言中超出该长度的位移量会先进行$mod\ w$运算。

  2. 在for循环里,如果非负的循环变量递减,最好不要用无符号数,比如下面代码会陷入死循环,因为判断语句按照无符号数来解释,而所有的无符号数都大于等于0:

    for(unsigned int i=n-2;i>=0;i--) 
        a[i]+=a[i+1]
    
  3. 注意在语句

    int x=10;
    printf("%p %x",&x,&x);
    >>(例) 0x100402010 402010
    

    中,%p将相应值作为指针(即地址)来看待,而%x将相应值作为无符号数来看待,因此在32位系统中,二者等价,但64位系统中,使用%x只会显示低地址处的32位,而%p则会完整地显示地址。

  4. 在IEEE的浮点表示中,一个很有趣的现象是,它表示的数,越接近0越稠密,并非均匀分布,但是在极接近0的时候又是均匀分布的,如下图:

    data_5

    有一个问题是,对于小数字段有$n$位的浮点数来说,这种表示法不能准确描述的最小正整数是什么?(假设阶码范围不会限制这个问题)

    先考虑这种方式能准确表示的最大正整数,首先想到数$\underbrace{ 1\cdots11 }_{n+1}=1.\underbrace{ 1\cdots11 }_{n}\times 2^n$,小数字段全为1,正好是小数点后$n$位中最大的数;然后我们将其加1,得到$1\underbrace{ 0\cdots00 }_{n+1}=1\times 2^{n+1}$,相当于将溢出部分加到了阶码位,仍然可以表示;然后再加1,即$1\underbrace{ 0\cdots00 }_{n}1=1.\underbrace{ 0\cdots00 }_{n}1 \times 2^{n+1}$,此时小数点后有$n+1$位,无法用$n$位小数准确表示。因此这个数应该是$2^{n+1}+1$。

  5. 有这么一个问题:

    double a=0.4,b=1.3,c=1.4;
    a+b==1.7 //False
    b+c==2.7 //True
    

    前者看起来很容易解释,因为这两个浮点数无法准确表示,所以加和很接近1.7但并不等于1.7。那么为什么后者是对的呢?

    在说浮点数表示不精确的时候,我们实际的意思是由于double类型小数位只有52位,而原数值的小数部分表示成二进制形式是无限循环小数,所以超出部分进行了舍入操作

    为什么前者是错的呢?首先看一下计算机中这些数的实际表示:

    十进制 浮点表示(hex) 符号位 阶码位 小数位
    0.4 0x 3f d9 99 99 99 99 99 9a 0 3fd 9 99 99 99 99 99 9a
    1.3 0x 3f f4 cc cc cc cc cc cd 0 3ff 4 cc cc cc cc cc cd
    0.4+1.3 0x 3f fb 33 33 33 33 33 34 0 3ff b 33 33 33 33 33 34
    1.7 0x 3f fb 33 33 33 33 33 33 0 3ff b 33 33 33 33 33 33

    用科学计数法来表示就是$0.4=(1.[0110])_2\times 2^{-2}$,$1.3=(1.0[1001])_2\times 2^0$

    这两者本身就存在舍入:

    1. 0.4小数部分第52位前后是 1001|1001,后面部分大于0.5,向上舍入,即$0.1001\cdots1010$

    2. 1.3小数部分第52位前后是 1100|1100,后面部分大于0.5,向上舍入,即$0.0100\cdots1101$

    之后将两者相加,首先将阶码位较小的数化为与较大数相同的阶码,具体到这里就是要将0.4的小数位右移2位,问题就出现在这里,右移时必然会溢出2位,那么就牵扯到舍入的问题,有两种策略:

    1. 0舍1入,若移出的那一位为1,那么在尾数末尾加1,若移出的那一位为0,那么直接舍弃。
    2. 恒置1,不管移出的是0或1,均在尾数末尾加1。

    这两种策略隐含的一点是,右移时每次移动一位,然后就按照策略舍入,然后再移动。经过验证,这里采取的是第一种策略。

    那么对于0.4,最后四位是1010,要移出两位,先移出0,直接舍弃,再移出1,尾数末尾加一变为11,得到$0.0110\cdots0111$,然后再与1.3的小数部分按位相加,就可以得到$1.1011\cdots0100$。而$1.7=1.1[0110]\times 2^0$,舍入后小数位为$0.1011\cdots0011$,与上面计算得到的和不同。

    接下来再简要说明一下后者为什么是正确的,1.4的尾数是$0.0110\cdots0110$,1.3的尾数是$0.0100\cdots1101$,两者相加为$10.1011\cdots0011$,这里要右移一位,得到$1.0101\cdots1010$,恰好与2.7的尾数部分相同,因此相等。

    总结一下,浮点数加减造成的非直观的结果都是由舍入造成的,舍入存在于两个部分,一是浮点数本身的舍入,二是运算过程中右移的舍入,后者又存在两种可能,

Lab

一共需要完成15个函数,难度从1-4不等。难度4的一道都没做出来,还是在网上找到了大神的解法搞明白了,来源如下:博文1博文2

// bitAnd - x&y using only ~ and |
// Rating: 1
int bitAnd(int x, int y) {
  return ~((~x)|(~y));//摩根定律
}

// getByte - Extract byte n from word x
// Rating: 2
int getByte(int x, int n) {
    int tmp = x >> (n << 3); //将x的第n个字节移动到最右
    tmp = tmp & 0xFF; //只保留最右一个字节
    return tmp;;
}

// logicalShift - shift x to the right by n, using a logical shift
// Rating: 3
int logicalShift(int x, int n) {
    int tmp = ~(1 << 31); //实现((1<<31)-1)
    tmp = ((tmp >> n) << 1) + 1; //实现((1<<31)-1)>>(n-1),产生合适掩码
    tmp = tmp & (x >> n); 
    return tmp;
}
int logicalShift(int x, int n) {
    int tmp=1 << (31 + (~n + 1)) << 1;//实现1<<(32-n)
    tmp=tmp + ~0;//得到掩码1<<(32-n)-1
    return tmp & (x >> n);
}

思想就是找到合适的掩码,这里是变量tmp,关键点在于$0\le n\le 32$,直接移位会导致越界,做的过程中发现了一个小坑,对于

  1. 1<<32,编译器会发出警告,但仍进行移位,结果为0
  2. a=32; 1<<a,编译器不会警告,但自动取模,即1<<(a%32),结果为1

// bitCount - returns count of number of 1's in word
// Rating: 4
int bitCount(int x) {
    int count; 
    int tmpMask1 = (0x55)|(0x55<<8); 
    int mask1 = (tmpMask1)|(tmpMask1<<16); // 0x55555555
    int tmpMask2 = (0x33)|(0x33<<8);
    int mask2 = (tmpMask2)|(tmpMask2<<16); // 0x33333333 
    int tmpMask3 = (0x0f)|(0x0f<<8); 
    int mask3 = (tmpMask3)|(tmpMask3<<16); // 0x0f0f0f0f 
    int mask4 = (0xff)|(0xff<<16); // 0x00ff00ff
    int mask5 = (0xff)|(0xff<<8);  // 0x0000ffff
    count = (x&mask1)+((x>>1)&mask1); // op1
    count = (count&mask2)+((count>>2)&mask2); // op2 
    count = (count + (count >> 4)) & mask3; // op3
    count = (count + (count >> 8)) & mask4; // op4
    count = (count + (count >> 16)) & mask5; // op5
    return count; 
}

使用二分法的思想。首先统计x中每两位1的个数,然后是每四位1的个数,以此类推,最后得到x中1的个数。问题的关键在于如何存储中间结果。下面根据代码中的步骤分为op1-op5,以op1和op5为例,其余同理。

op1. 把x视为16组长度为2的0-1序列,将掩码也看成16组01组成的数。将x中每一组的两个数分别和掩码每一组的01进行与运算,然后加起来,将结果count也分为16组,每一组中的值就是对应x中每一组0-1序列中1的个数。

op5. 等式右边的count看做是2组长度为16的0-1序列,高低两部分的值分别是x的高低两部分的1的个数,将两部分的值相加再和掩码0xffff进行与运算就得到x中1的个数。

需要注意的是,前两步与后三步略有不同。对于前两步,以op1为例,长度为2的0-1序列最多有2个1,也就是需要2个二进制位来存储结果(1的个数),如果先加再与,加的过程可能会产生进位,相当于如果原本有2个1,结果却变成了0个1,所以要先与再加;op2同理。后三步以op3为例,等式右边的count看作是8组长度为4的0-1序列,每一组最大值为4,两组直接相加为8仍然可以用4个二进制位来表示,因此可以先加后与。


// bang - Compute !x without using !
// Rating: 4
int bang(int x) {
    int tmp = ~x + 1;// tmp = -x;
    tmp = x | tmp; // 非0最高位一定是1,因为正数和负数 或  
    tmp = tmp >> 31; //非0为0xff ff ff ff,0为0x 00 00 00 00
    return (tmp+1);
}

在做这道题的时候一直在考虑0与其他数在位运算上的差别,但是找到的结果都不能恰好输出需要的结果,而这道题又恰恰不允许使用!运算符,最后还是直接看了答案。

思想就是如果一个数非0,那么它与它的相反数的符号位一定不同,两者取或一定为1,只有0是例外。将符号位右移31位后,若符号位是1,那么结果是-1;若符号位是0,结果是0。最后再加1即可。


// tmin - return minimum two's complement integer
// Rating: 1
int tmin(void) {
    return 1<<31;
}

// fitsBits - return 1 if x can be represented as an n-bit, two's complement integer.
// Rating: 2
int fitsBits(int x, int n) {
    int shiftNumber= 32 + (~n + 1);// 32 - n
    return !(x^((x<<shiftNumber)>>shiftNumber));
}

假设x可以用n位来表示,那么若x为正数,则最高位为0,进行符号扩展至32位后,扩展的位均为0;若x为负数,则最高位为1,符号扩展后,扩展的位均为1。因此可以先将原数左移(32-n)位,再右移(32-n)位,若仍然与原数相同,那么x可以用n位来表示。


// divpwr2 - Compute x/(2^n), for 0 <= n <= 30
// Rating: 2
int divpwr2(int x, int n) {
    //全0或者全1
    int signx = x >> 31;
    int mask = (1 << n) + (~0);//得到2^n - 1
    int bias = signx & mask;//如果x是正数,则bias为0,即不用加,直接移位
    //如果x为负数,加上偏置量之后在移位
    return (x + bias) >> n;
}

问题在于对于右移操作,如果有溢出,相当于向下取整,而我们希望向0舍入,因此对于x为负数的情况,要加上偏移量$2^n-1$来保证向上取整。接下来就是判断x的符号了,仍然使用符号位右移31位得到-1或0作为掩码的技巧,来确定是否对x加上偏移量,最后右移n位。


// negate - return -x
// Rating: 2
int negate(int x) {
  return (~x) + 1; // 取反加1
}

// isPositive - return 1 if x > 0, return 0 otherwise
// Rating: 3
int isPositive(int x) {
    return !((x >> 31) | (!x)); //根据符号位判断,注意排除0这个特殊情况
}

// isLessOrEqual - if x <= y  then return 1, else return 0
// Rating: 3
int isLessOrEqual(int x, int y) {
    int singx = (x >> 31) & 1;
    int singy = (y >> 31) & 1; 
    int sing = (singx ^ singy) & singx; // 根据符号位判断结果
    int tmp = x + ((~y) + 1); // x - y
    tmp = ((tmp>>31)&1) & (!(singx ^ singy));// 保证singx和singy同号
    return (sing | tmp | ((!(x ^ y)))); 
}

这个题关键在于两者相减的时候可能会产生溢出,所以更像是用位操作来实现if语句,根据两个数的范围来确定返回值。

  1. 如果x为负,y为非负,那么返回1。变量sing保存这种情况的返回值,首先取出x和y的符号位,只有当sigx=1且sigy=0时,sing才为1。
  2. 计算x-y,若(在两者同号情况下)结果的符号位为1,那么返回1。变量tmp保存这种情况的返回值,若两者异号,那么一定是x>y,这里就会返回0。
  3. 最后再包含x==y的情况即可。

// ilog2 - return floor(log base 2 of x), where x > 0
// Rating: 4
int ilog2(int x) {
    int bitsNumber=0;
    bitsNumber=(!!(x>>16))<<4;
    bitsNumber=bitsNumber+((!!(x>>(bitsNumber+8)))<<3);
    bitsNumber=bitsNumber+((!!(x>>(bitsNumber+4)))<<2);
    bitsNumber=bitsNumber+((!!(x>>(bitsNumber+2)))<<1);
    bitsNumber=bitsNumber+(!!(x>>(bitsNumber+1)));
    //for non zero bitsNumber, it should add 0
    //for zero bitsNumber, it should subtract 1
    bitsNumber=bitsNumber+(!!bitsNumber)+(~0)+(!(1^x));
    //当x为0时,还需要减一才能得到正确值。
    return bitsNumber;
}

问题可以转化为,寻找x的二进制表示中最左边的1的位置。

这里仍然采用二分的思想,首先将x右移16位,若剩下的位数全为0,那么最左边的1一定在低16位,否则在高16位,那么就将bitsNumber先加上16作为基数,这里通过(!!x>>16)<<4的方式来同时兼顾这两种情况。然后将最左边的1所在的16位右移8位就可以再缩小一半的范围,以此类推。

一个特殊情况是,如果x=1,那么$\log_2(1)=0$,由于所有数都至少要一个二进制位来存储,所以对于这种情况还要将结果减一。由于在这种特殊情况下x=1,bitsNumber=0都是确定的数,所以通过(!!bitsNumber)+(~0)+(!(1^x))的方法来实现语句:如果x是1,那么最后结果减一,否则结果不变。


// float_neg - Return bit-level equivalent of expression -f for floating point argument f. When argument is NaN, return argument.
// Rating: 2
unsigned float_neg(unsigned uf) {
    unsigned result;
    unsigned tmp;
    result=uf ^ 0x80000000; //将符号位取反
    tmp=uf & (0x7fffffff);
    if(tmp > 0x7f800000)//此时是NaN
        result = uf;
    return result;
}

// float_i2f - Return bit-level equivalent of expression (float) x
// Rating: 4
unsigned float_i2f(int x) {
    int sign=x>>31&1;
    int i;
    int exponent; 
    int fraction; 
    int delta;
    int fraction_mask;
    if(x==0)
        return x;
    else if(x==0x80000000)
        exponent=158;
    else{
        if (sign)//turn negtive to positive
            x = -x;
        i = 30;
        while ( !(x >> i) )
            i--;
        exponent = i + 127;
        x = x << (31 - i);
        fraction_mask = 0x7fffff;//(1 << 23) - 1;
        fraction = fraction_mask & (x >> 8);
        x = x & 0xff;
        delta = x > 128 || ((x == 128) && (fraction & 1));
        fraction += delta;
        if(fraction >> 23) {
            fraction &= fraction_mask;
            exponent += 1;
        }
    }
    return (sign<<31)|(exponent<<23)|fraction;
}

根据IEEE表示法的构成方式入手,思路分为以下几步:

  1. x=0或TMin时,用IEEE的规则看,只有符号位为0/1,其余均为0,所以要单独处理:当x=0时,浮点形式的位表示也是全0,当x=TMin时,浮点形式的符号字段sign=1,阶码字段exp=31+127=158,小数字段为全0。
  2. 除去以上两种情况后,要把整数x表示成浮点形式,那么相当于将其用科学计数法表示,首先将符号字段提取出来,接下来将x表示成$(1.f_{n-1}\cdots f_1f_0)_2 \times 2^{exp-127}$的形式。
  3. 要想确定阶码字段,即表示成为$(1.f_{n-1}\cdots f_1f_0)_2$,要先找到x中最左边1的位置,程序中while循环结束后的i即为上式中的$n$,是将x变为区间$[1,2)$之间的数需要右移的位数,相当于乘上了$2^n$,所以$n=exp-127$,这样就得到了阶码字段的值。
  4. 要想确定小数字段,要将上一步中$n$位小数截断为23位。截断之后要考虑两方面:一是被截断的部分是否大于等于0.5,这种情况小数部分要加一;二是如果加一后导致小数点前的1产生进位,那么要将阶码字段加一。
  5. 最后将各部分按照IEEE规则组织起来即可。

// float_twice - Return bit-level equivalent of expression 2*f for floating point argument f.
// Rating: 4
unsigned float_twice(unsigned uf) {
    unsigned f = uf;
    if ((f & 0x7F800000) == 0) //若阶码字段全为0
    {
        //小数字段左移一位
        f = ((f & 0x007FFFFF) << 1) | (0x80000000 & f);
    }
    else if ((f & 0x7F800000) != 0x7F800000) // 若f不为NaN或无穷大
    {
        //阶码字段加一
        f =f + 0x00800000;
    }
    return f;
}

将浮点数乘二最简单的情况是将阶码字段加一,然而阶码字段全为0时,浮点数表示0-1之间很小的数,这时进行乘二操作,需要将小数字段左移,此时如果恰好小数字段最高位为1移到了阶码字段,结果也恰好能够被正确解释(IEEE表示法的妙处之一)。区分这两种情况之后,再排除NaN和无穷大即可。