Floating-point Number
Table of Contents
1. 浮点数
浮点表示通过将数字编码为
直到 20 世纪 80 年代,每个计算机制造商都设计了自己的表示浮点数的规则,以及对浮点数执行运算的细节。另外,它们常常不会太多地关注运算的精确性,而把实现的速度和简便性看得比数字精确性更重要。
大约在 1985 年,这些情况随着 IEEE 标准 754 的推出而改变了,这是一个仔细制订的表示浮点数及其运算的标准。这项工作是从 1976 年开始由 Intel 赞助的,在 8087 设计的同时,8087 是一种为 8086 处理器提供浮点支持的芯片。他们请 William Kahan(加州大学伯克利分校的一位教授)作为顾问,帮助设计未来处理器浮点标准。他们支持 Kahan 加人一个 IEEE 资助的制订工业标准的委员会。这个委员会最终采纳的标准非常接近于 Kahan 为 Intel 设计的标准。目前,实际上所有的计算机都支持这个后来被称为 IEEE 浮点的标准。这大大提高了科学应用程序在不同机器上的可移植性。
IEEE 754 浮点标准是定义在一组“小而一致”的原则上的,所以它实际上是相当优雅和容易理解的。
2. 二进制小数
理解浮点数的第一步是考虑含有小数值的二进制数字。首先,让我们来看看更熟悉的十进制表示法。十进制表示法使用的表示形式为:
数字权的定义与十进制小数点符号(‘.’)相关,这意味着小数点左边的数字的权是 10 的正幂,得到整数值,而小数点右边的数字的权是 10 的负幂,得到小数值。例如,
类似地,考虑一个形如
符号‘.’现在变为了二进制的点,点左边的位的权是 2 的正幂,点右边的位的权是 2 的负幂。例如,
Figure 1: 小数的二进制表示。二进制点左边的数字的权重形如
假定我们仅考虑有限长度的编码,那么十进制表示法不能准确地表达像
二进制小数表示 | 值 | 十进制 |
---|---|---|
3. IEEE 浮点表示
前一节中谈到的二进制小数是一种定点表示法,它有一个缺点:不能很有效地表示非常大的数字。例如,表达式
IEEE 754 浮点标准用
为符号位(sign),它决定这个数是负数( )还是正数( )。 称为尾数,或有效数字(significand),它是一个二进制小数。 称为阶码(exponent),它的作用是对浮点数加权。
我们先介绍单精度浮点格式,它用 32 位来表示浮点数,划分规则如下(如图 2 的上部分):
- 1 个单独的符号位
直接编码符号 。 - 8 位阶码字段
编码阶码 (编码规则后文会介绍)。 - 23 位小数字段
编码尾数 (编码规则后文会介绍)。
Figure 2: IEEE 单精度和双精度浮点格式
对于双精度浮点格式,它用 64 位来表示,如果阶码字段位数记为
3.1. 阶码和尾码的编码规则
根据 exp 的值,被编码的值可以分为三种不同的情况(最后一种情况有两个变种),如图 3 所示,下面针对这三种情况分别介绍阶码
Figure 3: 单精度浮点数值的分类(阶码的值决定了这个数是规格化的、非规格化的、或特殊值)
情况一:规格化的值(Normalized Values)
这是最普遍的情况。当 exp 的位模式既不全为 0,也不全为 1 时,都属于这类情况。这种情况下,阶码和尾码的编码规则定义如下:
上面尾码
情况二:非规格化的值(Denormalized Values)
当 exp 的位模式全为 0,所表示的数就是非规格化形式。这种情况下,阶码和尾码的编码规则定义如下:
上面尾码
注:如果和规格化值中阶码的定义一致,阶码“应该”定义为
情况三:特殊值
最后一类数值是当指阶码全为 1 的时候出现的。当小数域全为 0 时,得到的值表示无穷,当
3.2. 数字示例
图 4 展示了假定的 8 位浮点格式的示例,其中有
Figure 4: 8 位浮点格式的非负值示例
从 0 自身开始,最靠近 0 的是非规格化数。这种格式的非规格化数的
可以观察到最大非规格化数
当增大阶码时,我们成功地得到更大的规格化值,通过 1.0 后得到最大的规格化数。这个数具有阶码
这种表示具有一个有趣的属性, 假如我们将图 4 中的值的位表达式解释为无符号整数,它们就是按升序排列的,就像它们表示的浮点数一样。这不是偶然的——IEEE 如此设计格式就是为了浮点数能够使用整数排序函数来进行排序,这是通过精巧地定义 Bias 来实现的。
3.3. Tips: 为什么要有 Bias
The exponent does not have a sign; instead an exponent bias is subtracted from it (127 for single and 1023 for double precision). This, and the bit sequence, allows floating-point numbers to be compared and sorted correctly even when interpreting them as integers.
上面内容摘自:https://floating-point-gui.de/formats/fp/ ,关于为什么可以使用整数排序函数来对浮点数进行排序的细节可以参考上一节的例子。
4. 舍入(向偶数舍入)
因为表示方法限制了浮点数的范围和精度,浮点运算只能近似地表示实数运算。因此,对于值
图 5 举例说明了应用四种舍入方式,将一个金额数舍入到最接近的整数美元数。 向偶数舍入(round-to-even),也称为向最接近的值舍入(round-to-nearest),是默认的方式 ,试图找到一个最接近的匹配值。因此,它将 1.40 美元舍入成 1 美元,而将 1.60 美元舍入成 2 美元,因为它们是最接近的整数美元值。唯一的设计决策是确定两个可能结果中间数值的舍入效果。 向偶数舍入方式采取的方法是:将数字向上或者向下舍入,使得结果的最低有效数字是偶数。 因此,这种方法将 1.5 美元和 2.5 美元都舍入成 2 美元。
Figure 5: 以美元为例说明舍入方式(单位为美元)
向偶数舍入最初看上去好像是个相当随意的目标——有什么理由偏向取偶数呢?为什么不始终把位于两个可表示的值中间的值都向上舍入呢?使用这种方法的一个问题就是很容易假想到这样的情景:这种方法舍入一组数值,会在计算这些值的平均数中引人统计偏差。我们采用这种方式舍入得到的一组数的平均值将比这些数本身的平均值略高一些。相反,如果我们总是把两个可表示值中间的数字向下舍入,那么舍入后的一组数的平均值将比这些数本身的平均值略低一些。 向偶数舍入的优点是在大多数现实情况中避免了这种统计偏差。 在 50% 的时间里,它将向上舍入,而在 50% 的时间里,它将向下舍入。
采用向偶数舍入方式舍入到只有两位小数,数字 1.2349999 将舍入到 1.23,数字 1.2350001 将舍入到 1.24,因为 1.2349999 和 1.2350001 都不是两个可能值的正中间值,舍入到距离最近的可能值;而数字 1.2350000(恰好在 1.23 和 1.24 中间)和 1.2450000(恰好在 1.24 和 1.25 中间)采用向偶数舍入方式则都会舍入到 1.24 ,因为 4 是偶数。
相似地, 向偶数舍入法能够运用于二进制小数。我们将最低有效位的值 0 认为是偶数,值 1 认为是奇数。 一般来说,只有对形如
5. 浮点运算(不满足结合率和分配率)
浮点运算会出其不意,如下面 golang 代码并不会输出 10.0,误差会累积:
package main import "fmt" func main() { var n float64 = 0 for i := 0; i < 1000; i++ { n += 0.01 } fmt.Println(n) // 输出 9.999999999999831, // 如果 n 定义为 float 32,上一行会输出 10.0001335,其累积的误差更大 }
我们必须非常小心地使用浮点运算,因为浮点运算只有有限的范围和精度,而且 浮点运算不遵守普遍的算术属性,比如结合率和分配率。
在单精度浮点情况下,表达式
在单精度浮点情况下,表达式
6. 浮点数的精度
我们知道单精度浮点数可以表示大到
比如,12345.67890123 有 13 位有效数字,它转换为单精度浮点数后,无法再恢复为 12345.67890123 了,这说明单精度浮点数的“精度”达不到 13 位;而 12345.67890123 转换为双精度浮点数后,还可以恢复回来,但这个例子说明双精度浮点数的“精度”可能达到 13 位(事实上,双精度浮点数的“精度”超过了 13 位)。
package main import "fmt" func main() { var a float32 = 12345.67890123 fmt.Println(a) // 输出 12345.679,已经有舍入误差了,float32精度肯定不能达到13位 var b float64 = 12345.67890123 fmt.Println(b) // 输出 12345.67890123,没有舍入误差 }
关于浮点数的精度,下面直接给出结论:
1、 单精度浮点数的“精度”为 6 位有效数字 (更具体点,在其可表示范围内,所有 6 位有效数字的十进制定点数都可以安全地表达为单精度浮点数,大部分 7 位有效数字的十进制定点数可以安全地表达为单精度浮点数,少部分 8 位有效数字的十进制定点数可以安全地表达为单精度浮点数,没有一个 9 位有效数字的十进制定点数可以安全地表达为单精度浮点数)。
2、 双精度浮点数的“精度”为 15 位有效数字 (更具体点,在其可表示范围内,所有 15 位有效数字的十进制定点数都可以安全地表达为双精度浮点数,大部分 16 位有效数字的十进制定点数可以安全地表达为双精度浮点数,没有一个 17 位有效数字的十进制定点数可以安全地表达为双精度浮点数)。
关于本节内容,详情可参考:https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/
7. 使用浮点数应遵循的指导原则
使用浮点数应遵循的指导原则:
1、避免数量级相差巨大的数之间的加减运算( 如果你必须把多个相差巨大的浮点数求和,则应该先对其进行排序,然后从最小值开始把它们加起来 )。
2、避免等量判断(浮点数的相等性判断一定不能直接使用等号,要使用类似 abs(v1 - v2) < 0.000001
这样的判断来判断浮点数是否相等)。
3、处理舍入误差问题(可以采用二进制编码的十进制变量,如 Java 中的 BigDecimal 类型避免浮点误差;或者直接变换为整形,如表示人民币金额时可以采用按分保存,这样使用整数即可)。
参见:《代码大全(第 2 版)》12.3 浮点数
8. 实例:0.2 的双精度浮点表示
通过在线转换工具(如 http://www.binaryconvert.com/result_double.html ),我们可以得到 0.2 的双精度浮点比特位为:
下面来验证一下为什么这个比特位可以表示数字 0.2,按照节 3.1 的规则,有:
那么所编码的浮点数为:
下面使用工具 bc
来计算上面算子:
$ echo 'ibase=2;1.1001100110011001100110011001100110011001100110011010' | bc 1.6000000000000000888178419700125232338905334472656250 $ echo 'scale=60;1.6000000000000000888178419700125232338905334472656250/8' |bc .200000000000000011102230246251565404236316680908203125000000
上面结果就是 64 个比特位:
9. 参考
本文的大部分内容直接摘自《深入理解计算机系统(原书第 2 版)》2.4 浮点数
单精度和双精度的浮点格式可仿照节 3.2 ,还可参考:
https://en.wikipedia.org/wiki/Single-precision_floating-point_format
https://en.wikipedia.org/wiki/Double-precision_floating-point_format