【问题标题】:Is it possible to get 0 by subtracting two unequal floating point numbers?两个不相等的浮点数相减可以得到0吗?
【发布时间】:2015-04-13 00:05:37
【问题描述】:

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

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

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

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

【问题讨论】:

  • 我必须尝试所有双打组合,这需要一段时间:)
  • @Thirler 在我看来是时候使用 JUnit 测试了!
  • @bluebrain,我猜你的文字数字 2.000 等包含许多小数,用浮点数表示。所以最后一个在比较中不会用实际使用的数字来表示。
  • @Thirler 可能。 '你不能真正保证你分配给 float 或 double 的数字是准确的'
  • 请注意,在这种情况下返回 0 会导致难以调试的歧义,因此请确保您确实想要返回 0 而不是抛出异常或返回 NaN。

标签: floating-point double floating-accuracy ieee-754


【解决方案1】:

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

特别是,Java 编程语言需要支持 IEEE 754 非规范化浮点数和逐渐下溢,这使得证明特定数值算法的理想属性变得更加容易。如果计算结果是非规范化数字,则浮点运算不会“清零”。

如果FPUdenormalized numbers 一起使用,则不等数的减法永远不会产生零(与乘法不同),另请参阅this question

对于其他语言,这取决于。例如,在 C 或 C++ 中,IEEE 754 支持是可选的。

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

【讨论】:

  • 但是 OP 想了解 2/(a-b)。这可以保证是有限的吗?
  • 感谢您的回答,我添加了一个指向维基百科的链接,用于解释非规范化数字。
  • @Taemyr 查看我的编辑。除法实际上可以溢出。
  • @Taemyr (a,b) = (3,1) => 2/(a-b) = 2/(3-1) = 2/2 = 1 IEEE 浮点数是否如此,我不知道
  • @DrewDormann IEEE 754 对于 C99 也是可选的。见标准附录 F。
【解决方案2】:

作为一种解决方法,以下情况如何?

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

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

【讨论】:

  • 避免问题并一次性简化测试。我喜欢。
  • -1 如果a=b,您不应该返回0。在 IEEE 754 中除以 0 可以获得无穷大,而不是例外。您正在避免这个问题,所以返回 0 是一个等待发生的错误。考虑1/x + 1。如果x=0,那将导致1,而不是正确的值:无穷大。
  • @ColeJohnson 正确答案也不是无穷大(除非您指定限制来自哪一侧,右侧 = +inf,左侧 = -inf,未指定 = 未定义或 NaN)。
  • @ChrisHayes:这是对问题的有效答案,认识到问题可能是 XY 问题:meta.stackexchange.com/questions/66377/what-is-the-xy-problem
  • @ColeJohnson 返回0 并不是真正的问题。这就是OP在问题中所做的。您可以在该块的该部分放置一个例外或任何适合该情况的内容。如果您不喜欢返回0,那应该是对这个问题的批评。当然,像 OP 那样做并不值得对答案投反对票。这个问题与给定函数完成后的进一步计算无关。如您所知,该计划的要求必须返回0
【解决方案3】:

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

现在,a == b 返回 true 的唯一方法是 ab 包含完全相同的位。如果它们仅相差最低有效位,则它们之间的差异不会为 0。

编辑:

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

  1. “没有一个数字与自身比较”为假,但具有相同的位模式。

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

所以如果ab 都是Double.NaN,您将到达else 子句,但由于NaN - NaN 也返回NaN,您将不会被零除。

【讨论】:

  • 伊兰;不完全正确。 “不是数字比较”自身为假,但具有相同的位模式。还定义了-0.0与+0.0比较为真,它们的位模式不同。
  • @Bathsheba 我没有考虑这些特殊情况。感谢您的评论。
  • @Eran,非常好的一点,除以 0 将返回浮点数中的无穷大。将其添加到问题中。
  • @Prashant 但在这种情况下不会发生除法,因为 a == b 会返回 true。
  • 实际上你可以得到一个除以零的 FP 异常,这是 IEEE-754 标准定义的一个选项,尽管它可能不是大多数人对“异常”的意思" ;)
【解决方案4】:

这里不可能发生除以零的情况。

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),并且您可以提出任意问题。无需解释和引用规范。您甚至可以提出混合浮点数和整数问题,例如“这个特定的int log2(float) 算法是否正确?”。

【讨论】:

  • 能否请您添加指向 SMT Solver Z3 的链接和指向在线解释器的链接?虽然这个答案看起来完全合法,但有人可能会认为这些结果是错误的。
【解决方案5】:

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

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

当除法的结果太大而无法存储在双精度中时,即使分母非零,也会返回无穷大。

【讨论】:

    【解决方案6】:

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

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

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

    【讨论】:

      【解决方案7】:

      在 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 可以切换到不返回非规范化数字而是将其替换为 0 的模式。在该模式下,如果 a 和 b 是微小的规范化数字,其差值小于最小规范化数字但大于0, a != b 也不保证 a == b。

      “从不比较浮点数”是货物狂热的编程。在拥有“你需要一个 epsilon”的口头禅的人中,大多数人不知道如何正确选择那个 epsilon。

      【讨论】:

        【解决方案8】:

        我能想到一个你可能能够导致这种情况发生的情况。这是一个以 10 为底的类似示例 - 当然,这将发生在以 2 为底的情况。

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

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

        1.00e2-9.99e1

        Shift 为每个值赋予相同的指数

        1.00e2-0.999e2

        四舍五入到 3 位数

        1.00e2-1.00e2

        哦哦!

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

        【讨论】:

        • 保存对齐的减法操作数的寄存器需要保存额外的两位,称为“保护位”,以处理这种情况。在减法会导致从最高有效位借位的情况下,较小的操作数的大小必须超过较大操作数的一半(这意味着它只能具有额外的一位精度),否则结果必须至少为较小操作数大小的一半(意味着它只需要多一位,加上足以确保正确舍入的信息)。
        • “这最终能否发生取决于 FPU 设计” 不,它不可能发生,因为 Java 定义说它不能。 FPU设计与它无关。
        • @PascalCuoq:如果我错了,请纠正我,但 strictfp 未启用,计算产生的值可能对 double 来说太小,但适合扩展精度浮点值。
        • @supercatstrictfp 的缺失只会影响“中间结果”的值,我引用自docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.4abdouble 变量,不是中间结果,所以它们的值是双精度值,因此是 2^-1074 的倍数。因此,这两个双精度值的减法是 2^-1074 的倍数,因此更宽的指数范围确实会改变当 a == b 时差为 0 的性质。
        • @supercat 这是有道理的——你只需要一个额外的位就可以做到这一点。
        【解决方案9】:

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

        要合理比较浮点数是否相等,您需要检查该值是否“足够接近”相同的值:

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

        【讨论】:

        • “永远不应该”有点强烈,但通常这是个好建议。
        • 虽然你是真的,但abs(first - second) &lt; error(或&lt;= error)更简单、更简洁。
        • 虽然在大多数情况下都是正确的(不是全部),但并不能真正回答问题。
        • 测试浮点数是否相等通常很有用。与未经仔细选择的 epsilon 进行比较是不理智的,在测试相等性时与 epsilon 进行比较就更不理智了。
        • 如果您根据浮点键对数组进行排序,我可以保证如果您尝试使用将浮点数与 epsilon 进行比较的技巧,您的代码将无法工作。因为 a == b 和 b == c 的保证意味着 a == c 不再存在。对于哈希表,完全相同的问题。当相等性不具有传递性时,您的算法就会崩溃。
        【解决方案10】:

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

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

        double calculation(double a, double b)
        {
             if (a == b)
             {
                 return nan(""); // C++
        
                 return Double.NaN; // Java
             }
             else
             {
                 return 2 / (a - b);
             }
        }
        

        【讨论】:

          【解决方案11】:

          核心问题是当你有“太多”小数时,双精度(又称浮点数,或数学语言中的实数)的计算机表示是错误的,例如当你处理不能写成双精度的双精度时数值(pi 或 1/3 的结果)。

          所以a==b不能用a和b的任何double值来完成,当a=0.333和b=1/3时你如何处理a==b?根据您的操作系统、FPU、数字、语言以及 0 后的 3 计数,您将得到真或假。

          无论如何,如果您在计算机上进行“双值计算”,则必须处理准确性,因此您必须使用absolute_value(a-b)&lt;epsilon 而不是a==b,并且epsilon 与您在该处建模的内容有关算法中的时间。您不能对所有的双重比较都有一个 epsilon 值。

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

          PS:嗯,我这里回答的一切都或多或少在别人的回复和cmets中。

          【讨论】:

            【解决方案12】:

            基于@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 的最简单方法实际上是执行除法。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2019-10-29
              • 2014-09-29
              • 1970-01-01
              • 2018-12-10
              • 1970-01-01
              • 2017-03-14
              • 1970-01-01
              相关资源
              最近更新 更多