我认为 Higham 的数值算法的准确性和稳定性解决了如何分析这些类型的问题。参见第 2 章,尤其是练习 2.8。
在这个答案中,我想指出海厄姆的书中没有真正解决的问题(就此而言,它似乎并不广为人知)。如果您对证明诸如此类的简单数值算法的属性感兴趣,您可以使用现代 SMT 求解器(可满足性模理论)的强大功能,例如z3,使用Haskell中的sbv等包。这比使用铅笔和纸要容易一些。
假设给定 $0\leq x\leq y$,我想知道 $z=(x+y)/2$ 是否满足 $x\leq z\leq y$。下面的 Haskell 代码
import Data.SBV
test1 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test1 fun =
do [x, y] <- sFloats ["x", "y"]
constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
constrain $ 0 .<= x &&& x .<= y
let z = fun x y
return $ x .<= z &&& z .<= y
test2 :: (SFloat -> SFloat -> SFloat) -> Symbolic SBool
test2 fun =
do [x, y] <- sFloats ["x", "y"]
constrain $ bnot (isInfiniteFP x) &&& bnot (isInfiniteFP y)
constrain $ x .<= y
let z = fun x y
return $ x .<= z &&& z .<= y
会让我自动执行此操作。这test1 fun
是命题$x \leq \mathit{fun}(x,y) \leq y$ 对于所有有限浮点 $x,y$ 和 $0\leq x\leq y$。
λ> prove $ test1 (\x y -> (x + y) / 2)
Falsifiable. Counter-example:
x = 2.3089316e36 :: Float
y = 3.379786e38 :: Float
它溢出了。假设我现在采用您的另一个公式:$z=x/2+y/2$
λ> prove $ test1 (\x y -> x/2 + y/2)
Falsifiable. Counter-example:
x = 2.3509886e-38 :: Float
y = 2.3509886e-38 :: Float
不起作用(由于逐渐下溢:$(x/2)\times2\neq x$,由于所有算术都是以 2 为底的,这可能不直观)。
现在试试 $z=x + (yx)/2$:
λ> prove $ test1 (\x y -> x + (y-x)/2)
Q.E.D.
作品!这Q.E.D.
是一个证明该test1
属性适用于上面定义的所有浮点数。
相同的情况如何,但仅限于 $x\leq y$(而不是 $0\leq x\leq y$)?
λ> prove $ test2 (\x y -> x + (y-x)/2)
Falsifiable. Counter-example:
x = -3.1300826e34 :: Float
y = 3.402721e38 :: Float
好的,如果 $yx$ 溢出,那么 $z = x + (y/2-x/2)$ 怎么样?
λ> prove $ test2 (\x y -> x + (y/2 - x/2))
Q.E.D.
因此,似乎在我在这里尝试过的公式中, $x + (y/2 - x/2)$ 似乎有效(也有证明)。在我看来,SMT 求解器方法比用铅笔和纸进行浮点错误分析更快地回答了对简单浮点公式的怀疑。
最后,准确性和稳定性的目标往往与性能目标不一致。就性能而言,我真的看不出你能比 $(x+y)/2$ 做得更好,特别是因为编译器仍然会为你完成将其翻译成机器指令的繁重工作。
PS这都是单精度IEEE754浮点运算。我用双精度算术(替换SFloat
为SDouble
)检查了 $x \leq x + (y/2-x/2) \leq y$,它也可以工作。
PPS在代码中实现这一点时要记住的一件事是编译器标志-ffast-math
(某些形式的此类标志有时在某些常见编译器中默认打开)不会导致 IEEE754 算术,这将使上述证明无效。如果您确实使用了启用例如关联加法优化的标志,那么除了$(x+y)/2$ 之外,没有任何意义。
PPPS只看没有条件的简单代数表达式,我有点得意忘形。严格来说,Don Hatch的公式更好。