分类:data| 发布时间:2025-02-03 09:05:00
浮点表示对形如 V=x*2y 的有理数进行编码。 它对执行涉及非常大的数字(|V|>>0)、非常接近于 0(|V|<<1)的数字,以及更普遍地作为实数运算的近似值的计算,是很有用的。
理解浮点数的第一步是考虑含有小数值的二进制数字。 首先,让我们来看看更熟悉的十进制表示法。 十进制表示法使用的表示形式为: ,其中每个十进制数 di 的取值范围是 0~9。 这个表示方法描述的数值 d 定义如下:
数字权的定义与十进制小数点符号(‘.’)相关,小数点左边的数字的权是 10 的非负幂,得到整数值,而小数点右边的数字的权是 10 的负幂,得到小数值。 例如,12.3410 表示数字:1×101+2×100+3×10-1+4×10-2=12。
类似地,考虑一个形如 的表示法,其中每个二进制数字 bi 的取值范围是 0 和 1。 这种表示方法表示的数 b 定义如下:
符号 ‘.’ 现在变为了二进制的点,点左边的位的权是 2 的非负幂,点右边的位的权是 2 的负幂。 例如:101.112 表示数字 1×22+0×21+1×20+1×2-1+1×2-2=4+0+1++=5。
注意,形如 0.11...12 的数表示刚好小于 1 的数。 例如,0.1111112 表示 ,我们将用简单的表达法 1.0-ε 来表示这样的数值。
假定我们仅考虑有限长度的编码,那么十进制表示法不能准确地表达像 和 这样的数。 类似得,小数的二进制表示法只能表示能够被写成 的数。 其他的值只能够被近似地表示。
IEEE 浮点标准用 V=(-1)s×M×2E 的形式来表示一个数:
将浮点数的位表示划为三个字段,分别对这些值进行编码:
下图给出了将这三个字段装进字中两种常见的格式。 在单精度浮点格式(C 语言中的 float)中,s、exp 和 frac 字段分别为 1 位、k = 8 位和 n = 23 位,得到一个 32 位的表示。 在双精度浮点格式(C 语言中的 double)中,s、exp 和 frac 字段分别为 1 位、k = 11 位和 n = 52 位,得到一个 64 位的表示。
给定了位表示,根据 exp 的值,被编码的值可以分成三种不同的情况(最后一种情况有两个变种)。 下图说明了对单精度格式的情况。
下表展示了假定的 8 位浮点格式的示例,其中有 k = 4 的阶码位和 n = 3 的小数位。偏置值是 24-1-1=7。
描述 | 位表示 | 指数 | 小数 | 值 | |||||
---|---|---|---|---|---|---|---|---|---|
e | E | 2E | f | M | 2E×M | V | 十进制 | ||
0 | 0 0000 000 | 0 | -6 | 0 | 0.0 | ||||
最小的非规格化数 | 0 0000 001 | 0 | -6 | 0.001953 | |||||
0 0000 010 | 0 | -6 | 0.003906 | ||||||
0 0000 011 | 0 | -6 | 0.005859 | ||||||
⋮ | |||||||||
最大的非规格化数 | 0 0000 111 | 0 | -6 | 0.013672 | |||||
最小的规格化数 | 0 0001 000 | 1 | -6 | 0.015625 | |||||
0 0001 001 | 1 | -6 | 0.017578 | ||||||
⋮ | |||||||||
0 0110 110 | 6 | -1 | 0.875 | ||||||
0 0110 111 | 6 | -1 | 0.9375 | ||||||
1 | 0 0111 000 | 7 | 0 | 1 | 1 | 1.0 | |||
0 0111 001 | 7 | 0 | 1 | 1.125 | |||||
0 0111 010 | 7 | 0 | 1 | 1.25 | |||||
⋮ | |||||||||
0 1110 110 | 14 | 7 | 128 | 224 | 224.0 | ||||
最大的规格化数 | 0 1110 111 | 14 | 7 | 128 | 240 | 240.0 | |||
无穷大 | 0 1111 000 | — | — | — | — | — | — | ∞ | — |
可以观察到最大的非规格化数 和最小规格化数 之间的平滑转变。 这种平滑性归功于我们对非规格化数的 E 的定义。 通过将 E 定义为 1-Bias,而不是 -Bias,我们可以补偿非规格化数的尾数没有隐含的开头的 1 这一事实。
假如我们将上表的值的位表达式解释为无符号整数,它们是按升序排列的,就像它们表示的浮点数一样。 IEEE 如此设计格式是为了浮点数能够使用整数排序函数来进行排序。 当处理负数时,有一个小的难点,因为它们有开头的 1,并且它们是按照降序出现的,但是不需要浮点运算来进行比较也能解决这个问题。
根据上表中展示的 8 位格式,我们能够看出有 k 位阶码和 n 位小数的浮点表示的一般属性。
下表展示了一些非负浮点数的示例:
描述 | exp | frac | 单精度 | 双精度 | ||
---|---|---|---|---|---|---|
值 | 十进制 | 值 | 十进制 | |||
0 | 00...00 | 0...00 | 0 | 0.0 | 0 | 0.0 |
最小的非规格化数 | 00...00 | 0...01 | 2-23×2-126 | 1.4×10-45 | 2-52×2-1022 | 4.9×10-324 |
最大的非规格化数 | 00...00 | 1...11 | (1-ε)×2-126 | 1.2×10-38 | (1-ε)×2-1022 | 2.2×10-308 |
最小规格化数 | 00...01 | 0...00 | 1×2-126 | 1.2×10-38 | 1×2-1022 | 2.2×10-308 |
1 | 01...11 | 0...00 | 1×20 | 1.0 | 1×20 | 1.0 |
最大规格化数 | 11...10 | 1...11 | (2-ε)×2127 | 3.4×1038 | (2-ε)×21023 | 1.8×2308 |
因为表示方法限制了浮点数的范围和精度,浮点运算只能近似地表示实数运算。 因此,对于值 x,我们一般想用一种系统的方法,能够找到“最接近的”匹配值 x',它可以用期望的浮点形式表示出来。 这就是舍入(rounding)运算的任务。 一个关键的任务是在两个可能值的中间确定舍入方向。 一种可选择的方法是维持实际数学的下界和上届。 例如:我们可以确定可表示的值 x- 和 x+,使得 x 的值位于它们之间:x- ≤ x ≤ x+。 IEEE 浮点格式定义了四种不同的舍入方法。 默认的方法是找到最接近的匹配,而其他三种可用于计算上界和下界。
下表举例说明了应用四种舍入方式,讲一个金额数舍入到最接近的整数美元数。 向偶数舍入,也称为向最接近的值舍入,是默认的方式,试图找到一个最接近的匹配值。 因此,它将 1.40 美元舍入成 1 美元,而将 1.60 美元舍入成 2 美元,因为它们是最接近的整数美元值。 唯一的设计决策是确定两个可能结果中间数值的舍入效果。 向偶数舍入方式采取的方法是:将数字向上或者向下舍入,使得结果的最低有效数字是偶数。 因此,这种方法将 1.5 美元和 2.5 美元都舍入成 2 美元。
方式 | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
---|---|---|---|---|---|
向偶数舍入 | 1 | 2 | 2 | 2 | -2 |
向零舍入 | 1 | 1 | 1 | 2 | -1 |
向下舍入 | 1 | 1 | 1 | 2 | -2 |
向上舍入 | 2 | 2 | 2 | 3 | -1 |
其他三种方式产生实际值的确界。这些方法在一些数字应用中是很有用的。
向偶数舍入看上去好像是个相当随意的目标——有什么理由偏向取偶数呢? 为什么不始终把位于两个可表示的值中间的值都向上舍入呢? 使用这种方法的一个问题就是:这种方法舍入一组数值,会在计算这些值的平均数中引入统计偏差。 我们采用这种方式舍入得到的一组数的平均值将比这些数本身的平均值略高一些。 相反,如果我们总是把两个可表示值中间的数字向下舍入,那么舍入后的平均值将比这些数本身的平均值略低一些。 向偶数舍入在大多数现实情况中避免了这种统计偏差。 在 50% 的时间里,它将向上舍入,而在 50% 的时间里,它将向下舍入。
相似地,向偶数舍入法能够运用于二进制小数。 我们将最低有效位的值 0 认为是偶数,值 1 认为是奇数。 一般来说,只有对形如 XX⋯X.YY⋯Y100⋯ 的二进制位模式的数,这种舍入方式才有效,其中 X 和 Y 表示任意位值,最右边的 Y 是要舍入的位置。 只有这种位模式表示在两个可能的结果正中间的值。 例如,考虑舍入值到最近的四分之一的问题(也就是二进制小数点右边2位)。 我们将 10.000112() 向下舍入到 10.002(2),10.001102()向上舍入到 10.012(),因为这些值不是两个可能值的正中间值。 我们将 10.111002()向上舍入成 11.002(3),而 10.101002()向下舍入成 10.102(),因为这些值是两个可能值的中间值,并且我们倾向于使最低有效位为零。
IEEE 标准指定了一个简单的规则,用来确定诸如加法和乘法这样的算术运算的结果。 把浮点值 x 和 y 看成实数,而某个运算 ⊙ 定义在实数上,计算将产生 Round(x ⊙ y),这是对实际运算的精确结果进行舍入后的结果。 在实际中,浮点单元的设计者使用一些聪明的小技巧来避免执行这种精确的计算,因为计算只要精确到能够保证得到一个正确的舍入结果就可以了。 当参数中有一个特殊值(如 -0、-∞ 或 NaN)时,IEEE 标准定义了一些使之合理的规则。 例如,定义 1/-0 将产生 -∞,而定义 1/+0 将产生 +∞。
前面我们看到了整数(包括无符号和补码)加法形成了阿贝尔群。 实数上的加法也形成了阿贝尔群,但是我们必须考虑舍入对这些属性的影响。 我们将 x+fy 定义为 Round(x+y)。 这个运算的定义针对 x 和 y 的所有取值,但是虽然 x 和 y 都是实数,由于溢出,该运算可能得到无穷值。 对于所有 x 和 y 的值,这个运算是可交换的,也就是说 x+fy=y+fx。 另一方面,这个运算是不可结合的。例如,使用单精度浮点数,表达式 (3.14+1e10)-1e10 求值得到 0.0——因为舍入,值 3.14 会丢失。另一方面,表达式 3.14+(1e10-1e10)得到值3.14。 作为阿贝尔群,大多数值的浮点加法都有逆元,也就是说 x+f-x=0。 无穷(因为 +∞-∞=NaN)和 NaN 是例外情况,因为对于任何 x,都有 NaN+f=NaN。
另外,浮点数加法满足了单调性属性:如果 a ≥ b,那么对于任何 a、b 以及 x 的值,除了 NaN,都有 x+a ≥ x+b。 无符号或补码加法不具有这个实数(和整数)加法的属性。
浮点数乘法也遵循通常乘法所具有的许多属性。 我们定义 x*fy 为 Round(x*y)。 这个运算在乘法中是封闭的(虽然可能产生无穷大或 NaN),它是可交换的,并且它的乘法单位元为 1.0。 另一方面,由于可能发生溢出,或者由于舍入而失去精度,它不具有可结合性。 例如,在单精度浮点情况下,表达式(1e20*1e20)*1e-20 的值为 +∞,而 1e20*(1e20*1e-20) 将得出 1e20。 另外,浮点乘法在加法上不具备分配性。 例如,在单精度浮点情况下,表达式 1e20*(1e20-1e20) 的值为 0.0,而 1e20*1e20-1e20*1e20 会得出 NaN。
另一方面,对于任何 a、b 和 c,并且 a、b 和 c 都不等于 NaN,浮点乘法满足下列单调性:
此外,我们还可以保证,只要 a≠NaN,就有 a*fa ≥ 0。
所有的 C 语言版本提供了两种不同的浮点数据类型:float 和 double。 在支持 IEEE 浮点格式的机器上,这些数据类型就对应于单精度和双精度浮点。 另外,这类机器使用向偶数舍入的方式。 不幸的是,因为 C 语言不要求机器使用 IEEE 浮点,所有没有标准的方法来改变舍入的方式或者得到诸如 -0、+∞、-∞ 或者 NaN 之类的特殊值。
较新版本的 C 语言,包括 ISO C99,包含第三种浮点数据类型 long double。 对于许多机器和编译器来说,这种数据类型等价于 double 数据类型。 不过对于 Intel 兼容机来说,GCC 用 80 位“扩展精度”格式来实现这种数据类型,提供了比标准 64 位格式大得多的取值范围和精度。
当在 int、float 和 double 格式之间进行强制类型转换时,程序改变数值和位模式的规则如下(假设 int 是 32 位的):