凌云的博客

行胜于言

浮点数

分类:data| 发布时间:2025-02-03 09:05:00

浮点表示对形如 V=x*2y 的有理数进行编码。 它对执行涉及非常大的数字(|V|>>0)、非常接近于 0(|V|<<1)的数字,以及更普遍地作为实数运算的近似值的计算,是很有用的。

二进制小数

理解浮点数的第一步是考虑含有小数值的二进制数字。 首先,让我们来看看更熟悉的十进制表示法。 十进制表示法使用的表示形式为: dm dm-1 ... d1 d0. d-1 d-2 ... d-n ,其中每个十进制数 di 的取值范围是 0~9。 这个表示方法描述的数值 d 定义如下:

d= i=-n m 10i× di

数字权的定义与十进制小数点符号(‘.’)相关,小数点左边的数字的权是 10 的非负幂,得到整数值,而小数点右边的数字的权是 10 的负幂,得到小数值。 例如,12.3410 表示数字:1×101+2×100+3×10-1+4×10-2=1234100

类似地,考虑一个形如 bm bm-1 ... b1 b0. b-1 b-2 ... b-n 的表示法,其中每个二进制数字 bi 的取值范围是 0 和 1。 这种表示方法表示的数 b 定义如下:

b= i=-n m 2i× bi 等式 2.19

符号 ‘.’ 现在变为了二进制的点,点左边的位的权是 2 的非负幂,点右边的位的权是 2 的负幂。 例如:101.112 表示数字 1×22+0×21+1×20+1×2-1+1×2-2=4+0+1+12+14=534

注意,形如 0.11...12 的数表示刚好小于 1 的数。 例如,0.1111112 表示 6364,我们将用简单的表达法 1.0-ε 来表示这样的数值。

假定我们仅考虑有限长度的编码,那么十进制表示法不能准确地表达像 1357 这样的数。 类似得,小数的二进制表示法只能表示能够被写成 x×2y 的数。 其他的值只能够被近似地表示。

IEEE 浮点表示

IEEE 浮点标准用 V=(-1)s×M×2E 的形式来表示一个数:

  • 符号(sign) s 决定这个数是负数(s=1) 还是正数(s=0)。
  • 尾数(significand) M 是一个二进制小数,它的范围是 1~2-ε,或者 0~1-ε。
  • 阶码(exponent) E 的作用是对浮点数加权,这个权重是 2 的 E 次幂(可能是负数)。

将浮点数的位表示划为三个字段,分别对这些值进行编码:

  • 一个单独的符号位 s 直接编码 s。
  • k 位的阶码字段 exp = ek-1...e1e0 编码阶码 E。
  • n 位小数字段 frac = fn-1...f1f0 编码尾数 M,但是编码出来的值也依赖于阶码字段的值是否等于 0。

下图给出了将这三个字段装进字中两种常见的格式。 在单精度浮点格式(C 语言中的 float)中,s、exp 和 frac 字段分别为 1 位、k = 8 位和 n = 23 位,得到一个 32 位的表示。 在双精度浮点格式(C 语言中的 double)中,s、exp 和 frac 字段分别为 1 位、k = 11 位和 n = 52 位,得到一个 64 位的表示。

给定了位表示,根据 exp 的值,被编码的值可以分成三种不同的情况(最后一种情况有两个变种)。 下图说明了对单精度格式的情况。

  • 情况1:规格化的值
    这是最普遍的情况。当 exp 的位模式既不全为 0(数值 0),也不全为 1(单精度数值为 255,双精度数值为 2047)时,都属于这种情况。 在这种情况下,阶码字段被解释为以偏置(biased)形式表示的有符号整数。 也就是说,阶码的值是 E=e-Bias,其中 e 是无符号数,其位表示为 ek-1...e1e0,而 Bias 是一个等于 2k-1-1(单精度 127,双精度 1023)的偏置值。 由此产生指数的取值范围,对于单精度是 -126~+127,而对于双精度是 -1022~+1023。
    对小数字段 frac 的解释为描述小数值 f,其中 0 ≤ f < 1,其中二进制表示为 0.fn-1...f1f0,也就是二进制小数点在最高有效位的左边。 尾数定义为 M=1+f。 有时这种方式也称为隐含的以 1 开头的表示,因为我们可以把 M 看成一个二进制表达式为 1.fn-1fn-2...f0 的数字。 既然我们总是能够调整阶码 E,使得尾数 M 在范围 1 ≤ M < 2 之中(假设没有溢出),那么这种表示方法是一种轻松获得一个额外精度的技巧。
  • 情况2:非规格化的值
    当阶码域为全 0 时,所表示的数就是非规格化的形式。 在这种情况下,阶码值是 E = 1 - Bias,而尾数的值是 M = f,也就是小数字段的值,不包含隐含开头的 1。
    非规格化数由两个用途。 首先,它们提供了一种表示数值 0 的方法,因为使用规格化数,我们必须总是使 M ≥ 1,因此我们就不能表示 0。 实际上,+0.0 的浮点数表示的位模式为全 0。 当符号位为 1,而其他域全为 0 时,我们得到值 -0.0。 根据 IEEE 的浮点格式,认为值 +0.0 和 -0.0 在某些方面是不同的,而在其他方面是相同的。
    非规格化数的另外一个功能是表示那些非常接近于 0.0 的数。 它们提供一种属性,称为逐渐溢出,其中,可能的数值分布均匀地接近于 0.0。
  • 情况3:特殊值
    最后一类数值是当阶码全为 1 的时候出现的。 当小数域全为 0 时,得到的值表示无穷,当 s=0 时是 +∞,当 s=1 时是 -∞。 当小数域为非零时,结果值被称为“NaN”,就是“不是一个数”(Not a Number)的缩写。

下表展示了假定的 8 位浮点格式的示例,其中有 k = 4 的阶码位和 n = 3 的小数位。偏置值是 24-1-1=7。

描述位表示指数小数
eE2EfM2E×MV十进制
00 0000 000 0-6164 0808 051200.0
最小的非规格化数0 0000 001 0-6164 1818 151215120.001953
0 0000 010 0-6164 2828 251212560.003906
0 0000 011 0-6164 3838 351235120.005859
最大的非规格化数0 0000 111 0-6164 7878 751275120.013672
最小的规格化数0 0001 000 1-6164 0888 85121640.015625
0 0001 001 1-6164 1898 951295120.017578
0 0110 110 6-112 68148 1416780.875
0 0110 111 6-112 78158 151615160.9375
10 0111 000 701 0888 8811.0
0 0111 001 701 1898 98981.125
0 0111 010 701 28108 108541.25
0 1110 110 147128 68148 17928224224.0
最大的规格化数0 1110 111 147128 78158 19208240240.0
无穷大0 1111 000

可以观察到最大的非规格化数 7512 和最小规格化数 8512 之间的平滑转变。 这种平滑性归功于我们对非规格化数的 E 的定义。 通过将 E 定义为 1-Bias,而不是 -Bias,我们可以补偿非规格化数的尾数没有隐含的开头的 1 这一事实。

假如我们将上表的值的位表达式解释为无符号整数,它们是按升序排列的,就像它们表示的浮点数一样。 IEEE 如此设计格式是为了浮点数能够使用整数排序函数来进行排序。 当处理负数时,有一个小的难点,因为它们有开头的 1,并且它们是按照降序出现的,但是不需要浮点运算来进行比较也能解决这个问题。

根据上表中展示的 8 位格式,我们能够看出有 k 位阶码和 n 位小数的浮点表示的一般属性。

  • 值 +0.0 总有一个全为 0 的位表示。
  • 最小的正非规格化值的位表示,是由最低有效位 1 而其他所有位为 0 构成的。 它具有小数(和尾数)值 M = f = 2-n 和阶码值 E = -2k-1+2。 因此它的数字值是 V=2-n-2k-1+2
  • 最大的非规格化值的位模式是由全为 0 的阶码字段和全为 1 的小数字段组成的。 它有小数(和尾数)值 M = f = 1-2-n(我们写作 1-ε)和阶码值 E=-2k-1+2。 因此,数值 V=(1-2-n)*2-2k-1+2。这仅比规格化值小一点。
  • 最小的正规格化值的位模式的阶码字段的最低有效位为 1,其他位全为 0. 它的尾数值 M=1,而阶码值 E=-2k-1+2。因此,数值 V=2-2k-1+2
  • 值 1.0 的位表示的阶码字段除了最高有效位等于 0 以外,其他位都等于 1。 它的尾数值是 M=1,而它的阶码值是 E=0。
  • 最大的规格化值的位表示的符号位为 0,阶码的最低有效位等于 0,其他位等于 1。 它的小数值 f=1-2-n,尾数 M=2-2-n(我们写作 2-ε)。 它的阶码值 E=2k-1-1,得到数值 V=(2-2-n)*22k-1-1=(1-2-n-1)*22k-1

下表展示了一些非负浮点数的示例:

描述expfrac单精度双精度
十进制十进制
000...000...0000.000.0
最小的非规格化数00...000...012-23×2-1261.4×10-452-52×2-10224.9×10-324
最大的非规格化数00...001...11(1-ε)×2-1261.2×10-38(1-ε)×2-10222.2×10-308
最小规格化数00...010...001×2-1261.2×10-381×2-10222.2×10-308
101...110...001×201.01×201.0
最大规格化数11...101...11(2-ε)×21273.4×1038(2-ε)×210231.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(2332) 向下舍入到 10.002(2),10.001102(2316)向上舍入到 10.012(214),因为这些值不是两个可能值的正中间值。 我们将 10.111002(278)向上舍入成 11.002(3),而 10.101002(258)向下舍入成 10.102(212),因为这些值是两个可能值的中间值,并且我们倾向于使最低有效位为零。

浮点运算

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,浮点乘法满足下列单调性:

abc0 a*fc b*fc abc0 a*fc b*fc

此外,我们还可以保证,只要 a≠NaN,就有 a*fa ≥ 0。

C 语言中的浮点数

所有的 C 语言版本提供了两种不同的浮点数据类型:float 和 double。 在支持 IEEE 浮点格式的机器上,这些数据类型就对应于单精度和双精度浮点。 另外,这类机器使用向偶数舍入的方式。 不幸的是,因为 C 语言不要求机器使用 IEEE 浮点,所有没有标准的方法来改变舍入的方式或者得到诸如 -0、+∞、-∞ 或者 NaN 之类的特殊值。

较新版本的 C 语言,包括 ISO C99,包含第三种浮点数据类型 long double。 对于许多机器和编译器来说,这种数据类型等价于 double 数据类型。 不过对于 Intel 兼容机来说,GCC 用 80 位“扩展精度”格式来实现这种数据类型,提供了比标准 64 位格式大得多的取值范围和精度。

当在 int、float 和 double 格式之间进行强制类型转换时,程序改变数值和位模式的规则如下(假设 int 是 32 位的):

  • 从 int 转换成 float,数字不会溢出,但是可能被舍入。
  • 从 int 或 float 转换成 double,因为 double 有更大的范围,也有更高的精度,所以能够保留精确的数值。
  • 从 double 转换成 float,因为范围要小一些,所以值可能溢出称为 +∞ 或 -∞。另外,由于精确度较小,它还可能被舍入。
  • 从 float 或者 double 转换成 int,值将会向零舍入。例如 1.999 将被转换成 1,而 -1.999 将被转换成 -1。 进一步来说,值可能溢出。C 语言标准没有对这种情况指定固定的结果。 与 Intel 兼容的微处理器指定位模式[10...00](字长为 w 时的 TMinw)为整数不确定值。 一个从浮点数到整数的转换,如果不能为该浮点数找到一个合理的整数近似值,就会产生这样一个值。 因此,表达式 (int)+1e10 会得到 -21483648,即从一个正值变成了一个负值。