《CSAPP》读书笔记 -- 第2章:浮点数原理(小专题)

浮点数的作用:区别于整形数,用来表示小数。可以用来表示很大的数,或者非常接近0的小数,或者近似的做实数计算,浮点数的一般形式:x\times 2^y

IEEE(pronounced “Eye-Triple-Eee”)浮点数标准。

rounding:when a number cannot be represented exactly in the format and hence must be adjusted upward or downward。可以翻译为:舍入

十进制的小数表示:d_m d_{m-1} \cdots d_1 d_0 . d_{-1} d_{-2} \cdots d_{-n},写成数学表达式:

d = \sum_{i=-n}^m 10^i \times d_i

相应的,二进制也可以写成这种形式:

b = \sum_{i=-n}^m 2^i \times b_i

浮点数的表示

IEEE浮点数的格式:V = (-1)^s \times M \times 2^E

  • s是符号(Sign),s为0时是正,s为1时是负
  • M是有效数字(Significand,即 尾数
  • E是 指数,Exponent,也叫 幂数阶码
  • 隐含的 基数 是2

下图是浮点数的内存分布模型,首先是符号域,然后是指数域,最后是分数域:

浮点数内存模型
  • 符号位s个,符号位只需要一位,s=1
  • 指数位k个,指数域 exp=e_{k-1}\cdots e_1 e_0,用来计算指数E
  • 分数为n个,分数域 frac=f_{n-1}\cdots f_1 f_0,用来计算有效数字M

32位浮点数(单精度,float型)中,s=1,k=8,n=23;64位浮点数(双精度,double型)中,s=1,k=11,n=52。

浮点数总共分为三种类型,下图是它们在数轴上的显示:

浮点数三种类型

可以看到非正常化值集中在0附近,正常化值散布在整个数轴的空间,特殊值则只表示两个无穷值。

正常化值(Normalized Values)

exp 域既不是全0,也不是全1的时候,就是正常化值。

E = e - Bias,其中 e 就是 exp 域:e_{k-1}\cdots e_1e_0 的值(除去全0和全1之后,取值范围是1到2^k-2),Bias=2^{k-1}-1(单精度的时候是127,双精度的时候是1023),那么 E 的取值范围,单精度的时候是:-126 ~ +127,双精度的时候是:-1022 ~ +1023,其实 E 的算法就是 移码 的计算方法。

M = 1+f0\le f\lt 1,内存里只记录f,而1作为一个前导值计算时候再加上,所以f是分数域 frac0.f_{n-1}\cdots f_1f_0 这种形式

非正常化值(Denormalized Values)

当指数域全0,就是非正常化格式。

在这种情况下,指数值是 E = 1-Bias,也就是固定了,有效数字值 M = f 也就是没有前导1了。这个格式下可以表示0,因为正常化值中,一定有: M\ge 1,所以我们无法在正常化值格式下表示0。当符号位是0,有效数字 M=f=0,我们得到的就是+0.0,当符号位是1的时候就是-0.0

除了可以表示0,这个格式的另一个作用就是用来表示非常接近0的数。

特殊值(Special Values)

当指数域全1的时候,且分数域是全0,就表示无穷大,如果符号域为0,表示 +\infty,如果符号位是1,则表示 -\infty。无穷大可以作为溢出的结果,当我们用两个很大的数相乘,或者除以0;

当指数域全1,且分数域并非全0的时候,结果可以叫做:NaN(Not a Number的简写),这种值用来表示不能用实数或者无穷大表示的计算结果,比如计算:\sqrt{-1} 或者 \infty - \infty

下图是浮点数三种类型的光滑衔接:

浮点数三种类型光滑衔接

看完浮点数的设计和构造我们可以发现以下这些特点:

  • 从编码上有效数字域采用了无符号整数编码,而指数域采用了移码编码
  • 非正常化值均匀分布在0附近,间隔的大小是:2^{1-Bias} \times 2^{-n},图中是 \frac{1}{512}
  • 正常化值的间隔随着 2^E 变大而逐渐变大,也就是精度逐渐降低
  • 精度是分组的,以指数域增加1为一组,每组有 2^n 个数(n是有效数字域的位数)
  • 按照精度划分,非正常化值只占一组,特殊值按照精度也只占一组,正常化值占 2^{k}-2
  • 牢记:非正常化值的指数域全0,特殊化值的指数域全1,处于中间的就是正常化值了
  • 最高精度就是两个非正常化值的间隔,最低精度是最大的一组正常化值的相邻两数的间隔,这个最大间隔是:2^{Bias} \times 2^{-n}
  • 正常化值的第一组的精度和非正常化值的精度一样,也就是实现了无缝衔接,理解这个设计是关键

浮点数的计算

舍入

舍入四种策略

Rounding维基百科

[图片上传失败...(image-150a66-1544597813435)]

浮点数中使用的是:舍入到最近的偶数,因为舍入结果放大和缩小各占50%的概率,这样就可以防止最终结果偏大或者偏小。

下面是把浮点数舍入到小数点后两位数:

  • 10.00011_2(2\frac{3}{32}) -> 10.00_2(2) 不到一半,正常四舍五入
  • 10.00110_2(2\frac{3}{16}) -> 10.01_2(2\frac{1}{4}) 超过一半,正常四舍五入
  • 10.11100_2(2\frac{7}{8}) -> 11.00_2(3) 正好一半,保证最后一位是偶数,所以向上舍入
  • 10.10100_2(2\frac{5}{8}) -> 10.10_2(2\frac{1}{2}) 正好一半,保证最后一位是偶数,所以向下舍入

浮点数加减运算

基本性质

  • 相加可能产生 infinity 或者 NaN
  • 不满足交换律,不满足结合律(因为舍入会造成精度上的损失)
  • 加上0等于原来的数
  • 除了 infinity 和 NaN,每个数都有对应的相反数
  • 除了 infinity 和 NaN,满足单调性,即 a\ge b \rightarrow a+c\ge b+c
#include <iostream>

using namespace std;

int main()
{
    // 浮点数加法不满足交换律
    cout << 3.14 + 1e20 - 1e20 << endl;
    cout << 1e20 - 1e20 + 3.14 << endl;
    // 浮点数加法不满足结合律
    cout << (3.14 + 1e20) - 1e20 << endl;
    cout << 3.14 + (1e20 - 1e20) << endl;
    return 0;
}

运行结果:

0
3.14
0
3.14

具体细节

设两个浮点数 xy

\begin{cases} x=(-1)^{s_x} M_x 2^{E_x} \\ y=(-1)^{s_y} M_y 2^{E_y} \end{cases}

则浮点数加减运算结果为:

x\pm y = \left((-1)^{s_x}M_x 2^{E_x-E_y} \pm (-1)^{s_y}M_y \right)2^{E_y}

  1. 对阶:首先要把指数位(阶码)调成一样,并相应的使M移位,由于有效域左移会引起最高有效位丢失,误差大,所以采用右移,此时阶码要增加。所以对阶原则是:小阶向大阶看齐
  2. 有效数加减:简单的无符号数字相加减。
  3. 规格化:有效数求和结果可能大于1,那么就向右规格化:尾数右移1位,阶码加1。
  4. 舍入:对于右移出去的位,采取舍入
  5. 检查阶码是否溢出
    • 阶码下溢:运算结果为非规格化数
    • 阶码上溢:置溢出标志

浮点数加减实例

x=3.14, y=2.718z=x+y

首先算出 xy 的内存表示:

x = 3+0.14,3的二进制表示是11,0.14的二进制要稍微计算一下,我们让0.14不断的乘以2(也就是左移),得到的整数位部分就是其二进制值的一位:

0.14 * 2 = 0.28     0
0.28 * 2 = 0.56     0
0.56 * 2 = 1.12     1
0.12 * 2 = 0.24     0
.
.
.

我们可以写个程序来完成这个计算工作:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>

using namespace std;

// 获取整形数的位数
int getDigits(int num){
    int count = 1;
    while(num/10>0){
        num %= 10;
        count++;
    }
    return count;
}

/**
 * 获取小数的二进制表示
 * @params precision 二进制表示精确到多少位
 * @params num 小数的整数表示
 */
char* getFloatBitset(int precision, int num){
    char* res = new char[precision];
    int digits = getDigits(num);
    int mod = pow(10, digits);
    char printFormat[50];
    sprintf(printFormat,"%%0.%df",2);
    // cout << printFormat <<endl;
    
    for(int i=0;i<precision;i++){
        printf(printFormat, num*1.0/mod);
        cout << " * 2 = ";
        num <<= 1;
        if(num >= mod){
            printf(printFormat, num*1.0/mod);
            cout << "       1" << endl;
            num %= mod;
            res[i] = '1';
        }else{
            printf(printFormat, num*1.0/mod);
            cout << "       0" << endl;
            res[i] = '0';
        }
    }
    return res;
}

/**
 * 获取小数的二进制表示
 * @params precision 二进制表示精确到多少位
 * @params num 浮点型小数
 * @params digits 输入的时候浮点型小数的位数
 */
char* getFloatBitset2(int precision, float num, int digits){
    char* res = new char[precision];
    int mod = pow(10,digits);
    // cout<<mod<<endl;
    char printFormat[50];
    sprintf(printFormat,"%%0.%df",2);
    for(int i=0;i<precision;i++){
        printf(printFormat, num);
        cout << " * 2 = ";
        num*=2;
        num = round(num*mod)/mod;
        if(num >= 1){
            printf(printFormat, num);
            cout << "       1" << endl;
            num -= 1;
            res[i] = '1';
        }else{
            printf(printFormat, num);
            cout << "       0" << endl;
            res[i] = '0';
        }
    }
    return res;
}

int main(int argc, char* argv[]){
    // char* res = getFloatBitset(atoi(argv[1]), atoi(argv[2]));
    char* res = getFloatBitset2(atoi(argv[1]), atof(argv[2]), atoi(argv[3]));
    cout << res << endl;
    return 0;
}

上面代码保存成:float2Bitset.cpp文件,然后编译,并使用:

$ g++ -o float2Bitset float2Bitset.cpp
$ ./float2Bitset 23 0.14 2

小数位精确到23位的话,3.14的定点浮点数表示是:11.00100011110101110000101

转成浮点数,首先规格化M,那么整体要右移1位,指数是1,由 E = e-BiasE=1, Bias=127e=128,也就是:1000 0000

最终3.14的内存表示是:
\underbrace{0}_{Sign}~\underbrace{10000000}_{Exponent}~~\underbrace{10010001111010111000011}_{Significand}

同样的方法得到2.718的内存表示:

\underbrace{0}_{Sign}~\underbrace{10000000}_{Exponent}~~\underbrace{01011011111001110110110}_{Significand}

这两个数恰好是同阶的,那么就不需要对阶操作了。将M相加,但这个数太长了看着眼花,我们写个加法程序:

#include <iostream>

using namespace std;

// 将两个相同位数的二进制数相加
char* addBitset(char num1[], char num2[], int length){
    char* res = new char[length+2];
    res[length+1] = '\0';
    int carry = 0;
    for(int i=length-1;i>=0;i--){
        res[i+1] = num1[i]-'0'+num2[i]-'0'+carry+'0';
        carry = 0;
        if(res[i+1]>'1'){
            res[i+1] -= 2;
            carry = 1;
        }
    }
    if(carry){
        res[0]='1';
    }else{
        res[0]='0';
    }
    return res;
}

int main(int argc, char* argv[]){
    int i=0;
    while(argv[1][i]!='\0'){
        i++;
    }
    cout << i <<endl;
    char* res = addBitset(argv[1], argv[2], i);
    cout << res << endl;
    return 0;
}

上述代码保存成:addBitset.cpp,编译并使用该程序:

$ g++ -o addBitset addBitset.cpp
$ ./addBitset 10010001111010111000011 01011011111001110110110

相加结果等于:0 11101101110100101111001,最高位没有产生进位,这里用了一个0来代替,但两个前导1相加产生了进位,所以还需要对M右归一下,再对指数加1。所以加法结果的浮点数表示是:

\underbrace{0}_{Sign}~\underbrace{10000001}_{Exponent}~~\underbrace{01110110111010010111101}_{Significand}

这个数的十进制表示的计算方法是:
2^2 \times (1+0\times (\frac{1}{2})^1 + 1\times (\frac{1}{2})^2 + 1\times (\frac{1}{2})^3 +1\times (\frac{1}{2})^4+0\times(\frac{1}{2})^5+\cdots)

我们依然采用程序来计算这一长串二进制对应的十进制小数:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>

using namespace std;

double bitset2Float(char* num1, int length){
    double res = 0.0;
    int count=1;
    for(int i=0;i<length;i++){
        double temp = (num1[i]-'0')/pow(2,count);
        // cout << temp << endl;
        res += temp;
        count++;
    }
    return res;
}


int main(int argc, char* argv[]){
    int i=0;
    while(argv[1][i]!='\0'){
        i++;
    }
    double res = bitset2Float(argv[1],i);
    cout << res << endl;
    return 0;
}

上述代码保存为:Bitset2float.cpp,编译并执行:

$ g++ -o Bitset2float Bitset2float.cpp
$ ./Bitset2float 01110110111010010111101

对得到结果:0.46451.4645\times 2^2 = 5.858,而 3.14+2.718=5.858,这就说明我们的计算无误。

了解了浮点数加法的流程之后,最后我们回到最上面说的 浮点数加减法不满足交换律和结合律,从计算细节分析为什么不行。

首先 3.14 的浮点数表示我们已经计算过了,那么 1e20 的浮点数是多少呢?1e20也就是 10^{20},用辗转相除法可以得到其二进制表示。我们这里使用计算器工具,先输入一个1,然后开始输入0,我们可以观察到相应的二进制数也随着发生变化,最后停在了10^{19}

10的19次方的二进制

很遗憾的是64bit只能摆的下 10^{19}。我试了一下把源程序中的 1e20 换成 1e19 也是同样的结果。所以我们就使用 1e19 来分析这道题。

首先是M规格化,M右移63位,E加63,舍入M,那么 1e19 最终的双精度浮点数表示是:0 10000111110 0001010110001110010001100000100100010011110100000000

小阶向大阶看齐,3.14的阶是1,M需要右移62位,而M的精度才52,可想而知M就是0了。那么 3.14 + 1e19 的结果就是 1e19。1e20就更加不用说了。

浮点数乘除

基本性质

  • 相乘可能产生 infinity 或者 NaN
  • 不满足交换律,结合律,分配率(因为溢出会造成程序无法计算出正确的结果)
  • 乘以1会等于原来的数
  • 除了 infinity 和 NaN,满足单调性:a\ge b \rightarrow a\times c \ge b \times c

具体细节

设两个浮点数 xy

\begin{cases} x = \pm M_x 2^{E_x} \\ y = \pm M_y 2^{E_y} \end{cases}

则浮点数乘除运算结果是:

xy = \pm (M_x\times M_y)2^{E_x\pm E_y}

  1. 计算阶码,判断是否溢出
  2. 求有效数的乘积
  3. 有效数舍入
  4. 计算符号位

浮点数还有相当多的细节,可以参考:IEEE 754

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容