两个不相等的浮点数相减可以得到0吗?

Is it possible to get 0 by subtracting two unequal floating point numbers?

在下面的例子中是否可以除以 0(或无穷大)?

public double calculation(double a, double b)
{
     if (a == b)
     {
         return 0;
     }
     else
     {
         return 2 / (a - b);
     }
}

一般情况下当然不会。但是如果 ab 非常接近,那么 (a-b) 会因为计算的精度而导致 0 吗?

请注意,这个问题是针对 Java 的,但我认为它适用于大多数编程语言。

无论 a - b 的值如何,您都不会被零除,因为浮点除以 0 不会引发异常。它 return 无穷大。

现在,如果 ab 包含完全相同的位,那么 a == b 使 return 为真的唯一方法。如果它们仅相差最低有效位,则它们之间的差值不会为 0。

编辑:

正如 Bathsheba 正确评论的那样,有一些例外:

  1. "Not a number compares" 自身为假,但具有相同的位模式。

  2. -0.0定义为与+0.0比较true,它们的位模式不同。

因此,如果 ab 都是 Double.NaN,您将到达 else 子句,但由于 NaN - NaN 也 returns NaN, 你不会被零除。

永远不要比较浮点数或双精度数是否相等;因为,您不能真正保证分配给 float 或 double 的数字是准确的。

要理智地比较浮点数是否相等,您需要检查值 "close enough" 是否与相同的值:

if ((first >= second - error) || (first <= second + error)

在 Java 中,如果 a != ba - b 永远不会等于 0。这是因为 Java 要求支持非规范化数字的 IEEE 754 浮点运算。来自 spec:

In particular, the Java programming language requires support of IEEE 754 denormalized floating-point numbers and gradual underflow, which make it easier to prove desirable properties of particular numerical algorithms. Floating-point operations do not "flush to zero" if the calculated result is a denormalized number.

如果 FPU works with denormalized numbers, subtracting unequal numbers can never produce zero (unlike multiplication), also see this question.

对于其他语言,视情况而定。例如,在 C 或 C++ 中,IEEE 754 支持是可选的。

也就是说,it is possible 表达式 2 / (a - b) 溢出,例如 a = 5e-308b = 4e-308

作为变通方法,以下情况如何?

public double calculation(double a, double b) {
     double c = a - b;
     if (c == 0)
     {
         return 0;
     }
     else
     {
         return 2 / c;
     }
}

这样您就不会依赖任何语言的 IEEE 支持。

这里不可能被零除。

SMT Solver Z3 支持精确的 IEEE 浮点运算。让我们让 Z3 找到数字 ab 使得 a != b && (a - b) == 0:

(set-info :status unknown)
(set-logic QF_FP)
(declare-fun b () (FloatingPoint 8 24))
(declare-fun a () (FloatingPoint 8 24))
(declare-fun rm () RoundingMode)
(assert
(and (not (fp.eq a b)) (fp.eq (fp.sub rm a b) +zero) true))
(check-sat)

结果是UNSAT。没有这样的数字。

上面的 SMTLIB 字符串还允许 Z3 选择任意舍入模式 (rm)。这意味着结果适用于所有可能的舍入模式(其中有五种)。结果还包括任何变量可能是 NaN 或无穷大的可能性。

a == b 实现为 fp.eq 质量,因此 +0f-0f 比较相等。与零的比较也使用 fp.eq 实现。由于问题旨在避免被零除,因此这是适当的比较。

如果相等测试是使用按位相等实现的,+0f-0f 将是使 a - b 为零的方法。此答案的错误先前版本包含有关该案例的模式详细信息,以供好奇。

Z3 Online暂不支持FPA理论。这个结果是使用最新的不稳定分支得到的。它可以使用 .NET 绑定进行复制,如下所示:

var fpSort = context.MkFPSort32();
var aExpr = (FPExpr)context.MkConst("a", fpSort);
var bExpr = (FPExpr)context.MkConst("b", fpSort);
var rmExpr = (FPRMExpr)context.MkConst("rm", context.MkFPRoundingModeSort());
var fpZero = context.MkFP(0f, fpSort);
var subExpr = context.MkFPSub(rmExpr, aExpr, bExpr);
var constraintExpr = context.MkAnd(
        context.MkNot(context.MkFPEq(aExpr, bExpr)),
        context.MkFPEq(subExpr, fpZero),
        context.MkTrue()
    );

var smtlibString = context.BenchmarkToSMTString(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr);

var solver = context.MkSimpleSolver();
solver.Assert(constraintExpr);

var status = solver.Check();
Console.WriteLine(status);

使用 Z3 来回答 IEEE 浮动问题很好,因为它很难忽略案例(例如 NaN-0f+-inf),并且您可以提出任意问题。无需解释和引用规范。您甚至可以提出混合浮点数和整数的问题,例如 "is this particular int log2(float) algorithm correct?".

我可以想到您 可能 能够导致这种情况发生的情况。这是一个以 10 为基数的类似示例 - 实际上,这当然会发生在以 2 为基数的情况下。

浮点数或多或少以科学记数法存储 - 也就是说,存储的数字不是 35.2,而是更像 3.52e2。

为了方便起见,假设我们有一个以 10 为基数运算且精度为 3 位的浮点单元。从 10.0 减去 9.99 会发生什么?

1.00e2-9.99e1

移位使每个值具有相同的指数

1.00e2-0.999e2

四舍五入到 3 位数

1.00e2-1.00e2

呃哦!

这是否会发生最终取决于 FPU 设计。由于 double 的指数范围非常大,硬件在某些时候必须在内部舍入,但在上述情况下,内部只需多出 1 个数字就可以防止出现任何问题。

在符合 IEEE-754 的浮点实现中,每个浮点类型可以保存两种格式的数字。一 ("normalized") 用于大多数浮点值,但它可以表示的第二小的数字仅比最小的大一点点,因此它们之间的差异无法以相同的格式表示。另一种 ("denormalized") 格式仅用于第一种格式无法表示的非常小的数字。

有效处理非规范化浮点格式的电路非常昂贵,而且并非所有处理器都包含它。一些处理器提供了一个选择,要么对非常小的数字进行操作比对其他值的操作慢很多,要么让处理器简单地将对于规范化格式来说太小的数字视为零。

Java 规范暗示实现应该支持非规范化格式,即使在这样做会使代码 运行 变慢的机器上也是如此。另一方面,某些实现可能会提供允许代码更快 运行 的选项,以换取对值的稍微草率的处理,这些值对于大多数目的来说太小而不重要(在值太小的情况下重要的是,用它们进行计算的时间是真正重要的计算时间的十倍,这可能很烦人,因此在许多实际情况下,清零比缓慢但准确的算术更有用。

提供的函数确实可以return无穷大:

public class Test {
    public static double calculation(double a, double b)
    {
         if (a == b)
         {
             return 0;
         }
         else
         {
             return 2 / (a - b);
         }
    }    

    /**
     * @param args
     */
    public static void main(String[] args) {
        double d1 = Double.MIN_VALUE;
        double d2 = 2.0 * Double.MIN_VALUE;
        System.out.println("Result: " + calculation(d1, d2)); 
    }
}

输出为Result: -Infinity

当除法的结果太大而无法存储在双精度数中时,即使分母不为零,也会return计算无穷大。

根据@malarres 的回复和@Taemyr 的评论,这是我的小小贡献:

public double calculation(double a, double b)
{
     double c = 2 / (a - b);

     // Should not have a big cost.
     if (isnan(c) || isinf(c))
     {
         return 0; // A 'whatever' value.
     }
     else
     {
         return c;
     }
}

我的意思是说:要知道除法结果是 nan 还是 inf,最简单的方法实际上是执行除法。

在 IEEE 754 之前的过去,a != b 很可能并不意味着 a-b != 0,反之亦然。这是最初创建 IEEE 754 的原因之一。

对于 IEEE 754,几乎 有保证。 C 或 C++ 编译器可以执行比所需精度更高的操作。所以如果 a 和 b 不是变量而是表达式,那么 (a + b) != c 并不意味着 (a + b) - c != 0,因为 a + b 可以计算一次更高精度,一次不更高的精度。

许多 FPU 可以切换到不 return 非规范化数字但用 0 替换它们的模式。在该模式下,如果 a 和 b 是微小的规范化数字,其中差异小于最小归一化数但大于 0,a != b 也不能保证 a == b。

"Never compare floating-point numbers" 是货物崇拜编程。在拥有咒语 "you need an epsilon" 的人中,大多数人不知道如何正确选择那个 epsilon。

除以零是不确定的,因为正数的极限趋于无穷大,负数的极限趋于负无穷大。

不确定这是 C++ 还是 Java,因为没有语言标记。

double calculation(double a, double b)
{
     if (a == b)
     {
         return nan(""); // C++

         return Double.NaN; // Java
     }
     else
     {
         return 2 / (a - b);
     }
}

核心问题是,当您使用 "too much" 小数时,双精度(又名浮点数,或数学语言中的实数)的计算机表示是错误的,例如,当您处理不能写为数值(pi 或 1/3 的结果)。

所以 a==b 不能用 a 和 b 的任何双精度值来完成,当 a=0.333 和 b=1/3 时,你如何处理 a==b?根据你的 OS vs FPU vs number vs language vs count of 3 after 0,你会有 true 或 false。

无论如何,如果你在电脑上做"double value calculation",你必须处理准确性,所以而不是做a==b,你必须做absolute_value(a-b)<epsilon,epsilon是相对于您当时在算法中建模的内容。您不能对所有双重比较都使用 epsilon 值。

简而言之,当您键入 a==b 时,您将得到一个无法在计算机上翻译的数学表达式(对于任何浮点数)。

PS: 嗯,我在这里回答的都或多或少在其他人的回复和评论中。