• 文章
  • double - 及其用法
发布
2008 年 8 月 28 日 (最后更新:2008 年 8 月 30 日)

double - 及其用法

评分:3.1/5 (8 票)
*****
本文介绍 "double" 算术的一些特性,以及你可以和不可以做什么。
注意:需要 UTF-8 支持数学符号。

0. 动机
=============
在进行浮点运算时,可能会出现一些奇怪的效果,例如
1
2
3
4
for(double i=0.0; i!=5.5; i+=0.5)
{
        ... 
}

由于舍入误差,可能永远不会终止。特别是测试 (不) 等值通常不会得到期望的结果。一个常见的解决方法是使用一个 epsilon
1
2
3
4
5
6
bool is_equal(double d1, double d2) 
{
        if(abs(d1-d2)<epsilon)
                return true;
        return false;
}

然而,这个 "解决方案" 只在少数情况下适用 - 这样定义的等价性不再是等价关系(失去了传递性:a-b<ε 且 b-c<ε 并不能推导出 a-c<ε - 哎呀)。另一个问题是:epsilon 应该有多大?

计算机应该计算出正确的结果,或者说明它的不确定性。使用 epsilon 通常无法实现这一点。这就是为什么你应该对本文的其余部分感兴趣 ;-)


1. 数值类型
==================
IEEE 754 double 值由以下几部分组成:
1 位符号 (+ 或 -)
11 位指数(范围:0 到 1047,0 和 1047 是 "保留" 的)
52 位有效数字(也称为 "尾数")

则表示的数字为

[符号] 2指数-偏差[=1023]*1[有效数字]

其中 1 是一个隐含位,在有效数字位开始之前设置为 1。

对于归一化数字,有效数字的第一位是 1 且不保存,因此有效数字扩展到 53 位(显然,前导零可以省略,因此数字以非零开头,在二进制系统中碰巧总是 1)。

当且仅当 0 < 指数 < 2047 时,该值才被归一化。如果指数为零,IEEE 使用 "非归一化" 数字,则值为 [符号]2-1022*[有效数字],但不包含有效数字中的前导 1。请注意,在非归一化数字范围内的计算通常非常慢(指数中的 2047 保留给 "NaN (非数字)、±inf 及类似项")。

如果你出于某种原因需要位访问,可以在 *大端序* 机器上使用
1
2
3
4
5
6
7
8
9
union ieee_double
{
   double   a;
   struct { unsigned s : 1;
            unsigned e :11;
            unsigned h :20;
            unsigned l :32;
          } b;
};

小端序需要单独处理,但方式类似。

2. 操作
=============
假设舍入设置为 "舍入到最近" 和 "偶数舍入断点"(默认)

设 ∘ 表示 {+,-,*,/} 中的一个运算,设 ⊚ 表示相应的机器运算,即 {⊕,⊖,⊛,⊘} 中的一个。

设 x、y 和 z 为 IEEE 754 double 值。
设 a 为实数。
设 fl(a) 为最接近 a 的 IEEE 754 double 值。

IEEE 754 现在保证
(a) fl(x ∘ y) = x ⊚ y
推论
x ⊖ y = 0 ⇔ x = y
x ⊕ y = y ⊕ x
x ⊛ y = y ⊛ x
(b) fl(sqrt(x)) = msqrt(x),其中 msqrt 是 "机器 sqrt" 操作。
此外,还定义了一些 "非数字" 结果,例如 NaN(非数字)和 +/- 无穷大。

然而,在一般情况下,由于舍入误差
a ⊚ ( b ⊚ c ) ≠ ( a ⊚ b ) ⊚ c

3. Sterbenz 引理
=================
在进行 double 运算时,你最重要的朋友之一是 Sterbenz 引理,它可以表述为:
如果 y/2 ≤ x ≤ y 则
x ⊖ y = x - y
如果 x ⊖ y 不下溢

或者,用 "普通英语" 表述:如果 y 在 x/2 和 x 之间,那么机器减法 x ⊖ y 就会给你精确的结果。

4. 误差界
===============
给定 (1) 和 (2) 中的性质以及由此产生的引理 (3),可以为使用 +,-,*,/ 和 'sqrt' 的任何公式计算误差界。然而,这种计算本身很繁琐且容易出错,因此通常使用近似值。

利用 Sterbenz 引理,可以证明一些更简单的多项式求值误差界。我不会在这里展示它们,但你可以搜索例如:
- Fortune-van-Wyk 界
- Mehlhorn-Näher 界
- Mehlhorn-Neumaier 界
来找到它们。(注意:据我所知,所有这些论文都仅证明了它们对于整数操作数(在 doubles 中表示)的正确性。然而,它们对任何 double 输入都正确。)所有这些公式一个值得注意的美妙之处在于,实际的误差界可以使用 double 算术来计算。

5. 浮点过滤器
=========================
记住动机示例:等值测试。给定两个多项式 P 和 Q,现在可以在某些情况下确定 P(x) = Q(x)。
1
2
3
4
5
// equality test:
if(abs(P(x1) - Q(x2)) > ErrorBound(max(x1,x2)))
        return false; /* we know that for sure */
else
        // ??? 

(请注意,如果 P 或 Q 是多元的,max 最好使用 x1 和 x2 的上确界范数...)

如果你在编译时(或程序启动时)知道操作数的大小并且可以预先计算它,那么在这种情况下,你只需要进行一次简单的测试即可获得精确性。
问题:'???' 是什么?有几种选择。你可以使用任意精度数字类型的精确算术,你可以使用浮点展开,或者你可以执行惰性自适应求值(请注意,速度从左到右增加,但实现难度(以及想出正确公式的难度)也随之增加)。
这些方法超出了本文的范围(如果时间允许,我可能会写一篇续篇 ;-))。
在某些领域,另一种可能性是使用受控扰动,这意味着输入被转换成不会出现等值且可以显示不等值的情况(不精确,但可能鲁棒且足够,因为我们保证了某种属性(即,我们不仅仅说 "不相等",我们还确保它不是)。

6. 结论
=============
虽然计算误差界需要额外的工作,但运行时效率是一个小问题(在大多数情况下,只需一次比较即可证明结果的正确性)。同时,由于错误地假设等值而导致的许多退化情况可以避免,通常可以显著提高性能。

________________________________________________________________________________
如果您有任何评论、建议或改进,或者发现了拼写错误(或多个),请告知我。