原文来自
基于原笔记进行部分改动
对应于书本的2.4


对应于书本的2.4。


小结

  • 根据IEEE标准,可以将浮点数划分为 非标准化,标准化,特殊值。对于非标准化和标准化的 M,E 公式不同。
  • 在浮点数舍入时,常使用偶数舍入,能够得到更平均的值
  • 浮点数运算需要考虑 舍入 和 溢出的问题,对于 M 还需要考虑左移和右移的情况

1. 浮点数简介

浮点表示对形如 csapp L3 - 浮点数据类型 - 图1 的有理数进行编码,比较适用于非常大的数字、非常接近0的数字。它常常不会太多关注运算的准确性,而是把实现的速度和简便性看得比数字精确性更重要。1985年提出了IEEE标准754,仔细制定了表示浮点数机器运算的标准,后续所有计算机都支持这个标准,极大提高了程序可移植性。
首先会介绍IEEE浮点表示方法,然后由于数字不能精确被表示,所以会介绍浮点数的舍入问题。最后介绍浮点数相关的运算。

2. IEEE浮点表示

IEEE浮点表示使用 csapp L3 - 浮点数据类型 - 图2 表示数字。其中包含三部分:

  • 符号(Sign)s:用来确定V的正负性,当s=0时表示正数,s=1时表示负数。用一个单独的符号位直接进行编码。
  • 尾数(Significand)M: 是一个二进制小数,通常介于1和2之间的小数。使用n位二进制进行编码的小数。
  • 阶码(Exponent)E:对浮点数进行加权。使用k位进行编码的正数 csapp L3 - 浮点数据类型 - 图3 。使用无符号整数的解码方式

C语言中有单精度精浮点数float,其中s=1、k=8、n=23;还有双精度浮点数double,其中s=1、k=11、n=52。
image.png
我们可以根据尾数和阶码的不同取值,将其分成三种情况:

  1. 规格化的值(normalized):
    • csapp L3 - 浮点数据类型 - 图5 ,其中 csapp L3 - 浮点数据类型 - 图6 ,由此能将E重新投影到正负值,并且能够和非规格化进行平滑转变。
    • csapp L3 - 浮点数据类型 - 图7,因为我们可以通过调整E使得 csapp L3 - 浮点数据类型 - 图8 ,所以通过这种形式将尾数变成 csapp L3 - 浮点数据类型 - 图9 的形式,就能获得额外的精度。规格化数能够表示大范围的数。
  2. 非规格化的值(denorm):
    • csapp L3 - 浮点数据类型 - 图10 ,由此来保证和规格化值的连续性
    • csapp L3 - 浮点数据类型 - 图11。非规格化数能够表示正负0以及趋近于0的数。
  3. 特殊值(special):
    • exp 全为1,frac 全为0,表示无穷,s = 0是正无穷,s = 1是负无穷
    • exp 全为1,frac 不全为0,表示不存在的数:NAN

image.png
image.png
通过上面我们可以观察到几个现象

  1. 非规格数稠密地分布在靠近0的区域
  2. 有些数的间隔是等距的,因为当 exp 值不变,在 frac 尾数区域进行增加会乘上相同的指数
  3. 越大的数间隔越大,因为比较大的数,它的指数 2^E 会比较大,使得每次变化量会比较大。

我们以正数为例(s=0),说明几个比较特殊的值:

  • 0:只有非规格化才能表示0,exp 和 frac 全部为0时,结果为0。
  • 最小的正非规格化数:csapp L3 - 浮点数据类型 - 图14 是固定值,在 csapp L3 - 浮点数据类型 - 图15 取值范围内的最小值正数是 csapp L3 - 浮点数据类型 - 图16,则 csapp L3 - 浮点数据类型 - 图17 ,所以 csapp L3 - 浮点数据类型 - 图18
  • 最大的非规格化数:csapp L3 - 浮点数据类型 - 图19 是固定值 ,在 frac 取值范围内的最大值是 csapp L3 - 浮点数据类型 - 图20 ,则 csapp L3 - 浮点数据类型 - 图21,所以 csapp L3 - 浮点数据类型 - 图22
  • 最小规格化数:exp 最小为 csapp L3 - 浮点数据类型 - 图23,此时 csapp L3 - 浮点数据类型 - 图24。 frac 全为0时,M 取得最小值 1,所以 csapp L3 - 浮点数据类型 - 图25
  • 1:要让V = 1,很容易想到让csapp L3 - 浮点数据类型 - 图26,即 csapp L3 - 浮点数据类型 - 图27,此时 frac = [0,0,0…0], exp = [0,1,1…1]
  • 最大规格化数:exp 最大为 [1,1,1…0],此时 E = exp - Bias = 2^k - 2 - (2^{k-1} - 1) = 2^{k-1} - 1,M 取得最小值 2 - 2^{-n},所以 V = (2 - 2^{-n})\times 2^{2^{k-1} - 1}

image.png
IEEE设计的好处:

  1. 最大非规格化值 (1 - 2^{-n})\times 2^{2 - 2^{k-1}} 和最小规格化值 2^{2 - 2^{k-1}} 之间的幅度是 ,2^{-n} 是n位尾数所能表示的最小值,可以看成是光滑转变
  2. 从最小非规格化数到最大规格化数的位向量的变化是顺序的,和无符号整数的排序相同。所以除了NAN外,可以用无符号数的排序函数来对浮点数进行排序。 注意:负无穷转化为无符号数进行比较时会有问题。
  3. exp 设置为无符号数,再通过 Bias 转变为有符号数的方式,可以使得浮点数的比较结果 = 无符号整数的比较结果。如果让 exp 设置为有符号数,不会有这种效果
  4. M 设置为1 + frac 的形式,可以节省一位的空间,表示更大的范围。

2.1 十进制转为浮点数表示

步骤:

  • 计算 Bias = 2^{k-1}-1
  • 计算阶码的值 exp 和尾数的值 frac
  • 根据 exp 判断编码属于哪一类值,并使用不同的编码方式:规格化:E = exp - Bias,M = 1 + frac。非规格化:E = 1 - Bias,M = frac。特殊值:exp 全为1,根据 frac 判断是NAN还是无穷值。
  • 计算最终的值 V = (-1)^s \times M \times 2^E

样例:
以12345为例,我们可以推算单精度浮点数的编码:s=1、k=8、n=23

  1. 计算 Bias = 2^{k-1} - 1 = 2^7 - 1 = 127
  2. 将12345化为二进制数 [11000000111001]
  3. 首先将二进制数转为小于1的科学计数法 0.11000000111001 \times 2^{14},由于指数14不是 1-Bias = -126,所以是规格化数,可以将其转化为大于一的科学计数法 1.1000000111001 \times 2^{13},所以 M = 1.1000000111001, E = 13
  4. 因为 M = 1 + frac,所以 frac 的编码为 [1000000111001]。因为 E = exp - Bias = exp - 127,所以 exp = 127 + 13 = 140 = [10001100]。
  5. 将frac 和 exp 的二进制编码扩展到对应位数后拼接,补上符号位 = 最终结果 01000110010000001110010000000000

注意:记得要在exp前面补0,在frac后面补0。因为exp表示整数,frac表示小数


2.2 浮点数转为十进制表示

步骤

  1. 根据 exp 判断二进制编码属于哪一类值: 规格化,非规格化,特殊值
  2. 计算 Bias
  3. 根据二进制编码的类型,得出 E 和 M 的值
  4. 计算最终的值 V = (-1)^s \times M \times 2^E

样例:
将单精度编码 01000110010000001110010000000000 转变为十进制表示

  1. 由于k = 8,n = 23,exp = [10001100],因此这个数是一个规格化数
  2. 由于k = 8,因此可以计算出 Bias = 2^7 - 1 = 127
  3. 计算出 exp = 140,E = exp - Bias = 13。由于 M = 1 + frac,frac = [1000000111001],可以计算出 M = 1.1000000111001
  4. V = M \times 2^E = 1.1000000111001 \times 2^{13} = 12345

    3. 浮点数舍入

    浮点数由于有限的位数,所以对于真实值x,我们想要用一种系统的方法来找到能够用浮点数表示的“最接近的x”匹配值x’,这个过程就称为舍入
    常见的舍入方法有四种:向零舍入、向上舍入、向下舍入以及向偶数舍入。以十进制为例可以看以下表格
    image.png
    其中比较特殊的是向偶数舍入:如果处于中间值,就朝着令最后一个有效位为偶数来舍入;否则朝着最近的值舍入。比如1.40,由于靠近1就朝1舍入;1.6靠近2就朝2舍入;1.50位于十进制的中间值,就朝着偶数舍入,所以为2。
    向偶数舍入的意义:如果对一系列值进行向上舍入,则舍入后的平均值会比真实值更大;使用向下舍入,则舍入后的平均值会比真实值更小。通过向偶数舍入,每个值就有50%概率变大、50%概率变小,使得总的统计量保持较为稳定。
    十进制的中间值为 500…{10},比这个中间值大就向上舍入,比这个中间值小就向下舍入,否则朝着偶数舍入。
    而二进制的中间值为 100…
    {2},比如 101…{2} 就比中间值大, 001…{2}就比中间值小。而且二进制中,当最后一个有效位为0时,为偶数。比如
    image.png
    如图小数点后取两位有效位。中间值为.00100
  • 10.00011:由于011比中间值小,所以直接向下舍入,为10.00。
  • 10.00110:由于110比中间值大,所以直接向上舍入,为10.01。
  • 10.11100:由于100为中间值,而10.11最后一个有效位1为奇数,所以向上舍入为偶数11.00。
  • 10.10100:由于100为中间值,而10.10最后一个有效位0为偶数,所以直接向下舍入10.10。

    4. 浮点数运算

    浮点数运算无法直接通过在位向量上运算得到。

    4.1 浮点数乘法

    对于两个浮点数 (-1)^{s1} \times M{1} \times 2^{E_{1}} 和 (-1)^{s2} \times M_2 \times 2^{E_2},计算结果为 (-1)^s \times M \times 2^E,其中 s = s1~XOR~s2,M = M_1 \times M_2,E = E_1 + E_2。

  • 如果M >= 2,需要将frac右移一位,并对E加一。

  • 如果 E 超过了表示范围,就发生了溢出。上溢出会得到INF值,下溢出会得到 0。
  • 如果M超过了表示范围,对frac进行舍入。
  • 首先进行0检查,如果一个为0,就不会进行浮点数乘法

数学性质

  • 可交换
  • 不可结合:结合可能会出现溢出和不精确的舍入,比如 10^{20} (10^{20} 10^{-20}) = 10^{20},而 (10^{20}10^{20})10^{-20} = INF。
  • 不可分配:如果分配了可能会出现 NAN,比如10^{20}(10^{20}-10^{20}) = 0,而 10^{20} 10^{20} - 10^{20}*10^{20} = NAN。
  • 保证:只要 a ≠ NAN,那么 a*^{t}a >= 0
  • 单调性:a >= b && c >= 0 ,可以推出 a c >= b c

    4.2 浮点数加法

    对于两个(-1)^{s1} \times M{1} \times 2^{E_{1}} 和 (-1)^{s2} \times M_2 \times 2^{E_2},计算结果为 (-1)^s \times M \times 2^E,其中s和M是运算结果,E = max(E1,E2)
    image.png

  • 如果 M >= 2,则 frac 右移一位,并对 E 加一

  • 如果 M < 1,则 frac 左移一位,并对 E 减一
  • 如果 E 超过表示范围,就发生溢出
  • 如果 M 超过表示范围,就对frac进行舍入

数学性质:

  • 由于溢出,可能得到无穷之。
  • 可交换
  • 不可结合(由于舍入),因为较大的数和较小的数相加,由于舍入问题,会将较小的数舍入,比如 csapp L3 - 浮点数据类型 - 图32csapp L3 - 浮点数据类型 - 图33
  • 除了无穷和NaN,存在加法逆元。
  • 满足单调性,如果 csapp L3 - 浮点数据类型 - 图34 ,则对于任意a、b和x,都有 csapp L3 - 浮点数据类型 - 图35 。NaN除外。无符号数和补码由于溢出会发生值的跳变,所以不满足单调性。

注意:需要考虑好清楚数值的范围,如果计算的数值范围变化很大,需要重新结合或改变运算顺序,避免由于溢出或舍入出现计算问题。


样例
步骤:

  1. 对阶:将两个操作数的小数点位置对齐,使得两个数的阶码相等。
  2. 尾数求和:对对阶后的尾数(frac) 按定点数进行运算,需要对结果进一步规格化处理
  3. 规格化:M >= 2,需要让frac右移,E + 1;M < 1,需要让 frac 左移一位,E 减一。
  4. 舍入:由于 frac 的位数有限,因此对运算结果舍入,还原成 IEEE的方法

求 1.010 2^2 + 1.110 2^3 的计算结果,假设尾数为3位

  1. 对阶:两个小数结合成为 (1.010 + 11.100 ) * 2^2
  2. 尾数求和:frac = (1.010 + 11.100)2^2 = (100.110) 2^2
  3. 规格化:(100.110) 2^2 = 1.00110 2^4
  4. 舍入:舍入时,1.001|10 向偶数舍入,则 1.001|10 = 1.010,最终结果 = 1.010 * 2^4

    5. C中的浮点数

    C中提供了float和double两种精度的浮点数。由于编码不同,所以在浮点数和整型数之间强制类型转换时,会修改编码,并且会出现溢出和舍入。
  • float/double转换成int:首先小数部分会被截断,也就是向0舍入。 在float不超过int范围时, float的尾数部分为23字节,比int的32字节小,int可以精确表示float的整数部分,而double的尾数有52位,可能会出现舍入;而当超过int的取值范围或NaN时,微处理器会指定 [100…0]为整数不确定值,即对应的TMIN,一个很大的浮点数转化为int时,可能会出现负数。
  • int或float转换为double:double尾数有52位,而int只有32位,float只有23位,所以double会精确表示int和float,不会出现溢出和舍入。
  • int转换为float:不会发生溢出,但是由于float尾数位数比较少,会出现舍入。
  • double转换为float:可能会出现溢出和舍入。

总结:超过数值表示范围,会发生溢出;尾数较短,会发生舍入。


课堂作业:
image.png

  • x == (int)(float) x:x从int转化为float,会发生舍入,错误。
  • x == (int)(double) x:x从int转化为double,不会发生舍入,正确
  • f == (float)(double) f:f从float转化为float,不会发生舍入,正确
  • d == (double)(float) d:d从double转化为float,会发生舍入,错误
  • f == -(-f):只是改变了两次符号位,不会发生舍入,正确
  • 2/3 == 2/3.0:2/3会发生舍入,错误
  • d < 0.0 推出 ((d2) < 0.0):即使d2溢出后,也是NAN,因此不影响结果,正确
  • d > f 推出 -f > -d:改变符号位不会发生溢出,正确
  • d d >= 0.0:保证d不是NAN时,一定成立,dd >= 0.0,溢出时结果 = 正无穷
  • (d+f)-d == f:可能会舍入,错误