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

132

在以下示例中,是否可能出现除以0(或无穷大)的情况?

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

通常情况下,这是不可能的。但是如果ab非常接近,由于计算精度的原因,(a-b)的结果可能会为0吗?

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


49
我需要尝试所有的双倍组合,这可能需要一些时间 :) - Thirler
3
听起来是用JUnit测试的时候了! - Matt Clark
7
@bluebrain,我猜想你的字面数字2.000等包含了太多小数位,无法被浮点数所表示。因此,在比较中,最后几位将不会与实际使用的数字相匹配。 - Thirler
4
@Thirler可能是这个意思:'你不能确保将浮点数或双精度数分配的数字是精确的'。 - guness
4
请注意,在这种情况下返回0可能会导致难以调试的歧义,因此请确保您真的想返回0,而不是抛出异常或返回NaN。 - m0skit0
显示剩余13条评论
12个回答

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

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

如果FPU使用denormalized numbers,则减去不相等的数字永远不会产生零(与乘法不同),也请参见this question
对于其他语言,情况各不相同。例如,在C或C++中,IEEE 754支持是可选的。

话虽如此,对于表达式2 / (a - b)来说,它有可能溢出,例如当a = 5e-308b = 4e-308时。


4
OP想了解2/(a-b)是否一定是有限的。 - Taemyr
谢谢你的回答,我添加了一个维基百科链接来解释非规范化数。 - Thirler
3
看到我的编辑。这个除法实际上是可能溢出的。 - nwellnhof
@Taemyr (a,b) = (3,1) => 2/(a-b) = 2/(3-1) = 2/2 = 1。至于IEEE浮点数是否正确,我不知道。 - Cole Tobin
一个小修正:IEEE-754不是任何现有的C++标准所必需的,但是C99确实要求IEEE-754的兼容性。 - Drew Dormann
1
@DrewDormann IEEE 754对于C99也是可选的。请参阅标准的附录F。 - nwellnhof

50

作为解决方法,以下方案如何?

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

这样一来,您就不会在任何语言上依赖IEEE的支持。


6
同时避免问题并简化测试,我喜欢。 - Joshua
11
如果 a=b,你不应该返回 0。在 IEEE 754 中,除以 0 会得到无穷大而不是异常。你正在回避这个问题,所以返回 0 就像是一个等待发生的错误。考虑一下 1/x + 1。如果 x=0,结果将为 1,而不是正确的值:无穷大。 - Cole Tobin
5
@ColeJohnson 正确答案也不是无穷大(除非你指定极限从哪一侧逼近,右侧为正无穷,左侧为负无穷,未指定则为未定义或NaN)。 - Nick T
12
@ChrisHayes: 这是一个对问题的有效回答,承认该问题可能是一个XY问题:http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem - slebetman
17
返回 0 并不是问题的关键,这就是提问者在问题中所做的。你可以在代码块的那部分放置一个异常或者适当的内容来处理这种情况。如果你不喜欢返回 0,那应该批评问题本身。当然,像提问者一样做并不值得回答被点踩。这个问题与给定函数完成后的进一步计算无关。你只知道程序要求返回 0 - jpmc26
显示剩余6条评论

25
无论 a - b 的值是什么,你都不会得到一个除以零的结果,因为浮点数除以0不会抛出异常,而是返回无穷大。
如果 a == b 为真,则唯一的可能是 ab 包含了完全相同的二进制位。 如果它们只是最低有效位不同,它们之间的差别将不是0。
编辑:
正如Bathsheba正确评论的那样,有一些例外情况:
1. "非数字"与自身比较结果为false,但具有相同的位模式。 2. -0.0被定义为与+0.0相等,它们的位模式不同。
因此,如果ab都是Double.NaN,则您将执行else语句,但由于NaN - NaN也返回NaN,因此您不会除以0。

11
Eran,严格来说不是这样的。 "非数字"与自身进行比较会返回假,但它们的二进制模式相同。此外,-0.0被定义为与+0.0比较为真,它们的二进制模式不同。 - Bathsheba
1
@Bathsheba 我没有考虑到这些特殊情况。感谢您的评论。 - Eran
2
@Eran,非常好的一点是在浮点数中被0除将返回无穷大。已将其添加到问题中。 - Thirler
2
@Prashant 在这种情况下不会进行除法,因为a == b将返回true。 - Eran
3
实际上,根据IEEE-754标准,除零操作可能会引发FP异常,尽管这可能与大多数人对“异常”的理解不太一样 ;) - Voo
显示剩余2条评论

17

这里没有可能会发生除以零的情况。

SMT求解器 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求解器Z3的链接和一个在线解释器的链接?虽然这个答案看起来完全正确,但有人可能会认为这些结果是错误的。 - A.L

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

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

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

当除法的结果太大无法存储在double类型中时,即使分母不为零,也会返回无穷大。


6
在符合IEEE-754规范的浮点实现中,每个浮点类型可以使用两种格式保存数字。其中一种(“归一化”)用于大多数浮点值,但它能表示的第二小的数字只比最小值略大一点,因此它们之间的差别在相同的格式中无法表示。另一种(“非规格化”)格式仅用于非常小的数字,这些数字在第一种格式中无法表示。
为了有效地处理非规格化的浮点格式,电路成本很高,并且并非所有处理器都包含它。一些处理器提供选择,要么使对非常小的数字进行的操作比其他值慢得多,要么使处理器将对于归一化格式太小的数字视为零。
Java规范暗示实现应支持非规格化格式,即使在这样做会使代码运行更慢的机器上也是如此。另一方面,有可能一些实现可能提供选项以允许代码以交换稍微粗略处理大多数情况下都不重要的值的方式运行得更快(在值太小而不重要的情况下,将具有这些值的计算花费时间的十倍比确保准确性更慢,在许多实际情况下,清零法则比慢但准确的算术更有用)。

6
在IEEE 754出现之前的古老时代,a != b 并不意味着 a-b != 0,反之亦然。这也是创建IEEE 754的原因之一。

有了IEEE 754,几乎可以保证。C或C++编译器允许使用比所需精度更高的操作。因此,如果a和b不是变量而是表达式,则(a + b) != c并不意味着(a + b) - c != 0,因为a + b可能会被计算两次,一次精度更高,一次不高。

许多浮点单元可以切换到不返回非规格化数字的模式,而是用0替换它们。在这种模式下,如果a和b是微小的规范化数字,其差异小于最小的规范化数字但大于0,则a!= b也不能保证a == b。

“永远不要比较浮点数”是灵异信仰式的编程。在那些奉行“你需要一个epsilon”的人中,大多数人都不知道如何正确选择epsilon。


2

我可以想到一种情况,你也许能够导致这种情况发生。这里举一个十进制的类比实例 - 当然,在二进制下会更容易发生。

浮点数以科学计数法存储 - 也就是说,不是看到35.2,被存储的数字更像是3.52e2。

为了方便起见,想象一下我们有一个在十进制下运行并具有3位精度的浮点单元。当你从10.0中减去9.99时会发生什么?

1.00e2-9.99e1

将每个值移动到相同的指数

1.00e2-0.999e2

四舍五入保留3个数字

1.00e2-1.00e2

糟糕!

最终是否会发生此类情况取决于FPU设计。由于双精度指数范围非常大,硬件必须在某个时间点内部进行舍入,但在上述情况下,只要内部多保存1位有效数字就可以避免任何问题。


1
执行减法的对齐操作数所占用的寄存器需要额外的两个位,称为“保护位”,以处理这种情况。在减法会导致从最高有效位借位的情况下,较小操作数的大小必须超过较大操作数的一半(意味着它只能具有一个额外的精度位),否则结果必须至少是较小操作数大小的一半(意味着它只需要一个额外的位,加上足够确保正确舍入的信息)。 - supercat
1
“这是否可能发生最终取决于FPU的设计。”不,它不可能发生,因为Java的定义说它不能。FPU的设计与此无关。 - Pascal Cuoq
@PascalCuoq:如果我错了,请纠正我,但是strictfp没有启用,计算可能会产生值,这些值对于double来说太小,但适合于扩展精度浮点值。 - supercat
这是有道理的 - 你只需要一个额外的比特来实现这个。 - Keldor314
@PascalCuoq:将“m”称为最小的正双精度浮点数。如果两个数字都可以表示为“double”,则它们之间的差将是“m”的倍数。那么,肯定有“double d=m1.125;”会将“d”设置为“m”,但我不确定“m==m1.125;”是否保证返回true。 - supercat
显示剩余3条评论

1

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

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

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

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

1
你不应该直接比较浮点数或双精度数是否相等,因为你不能保证所赋的数值是准确的。为了合理地比较浮点数是否相等,你需要检查这些数值是否“足够接近”相同的数值:
if ((first >= second - error) || (first <= second + error)

6
“Shouldn't ever”有点过于强烈,但总体来说这是很好的建议。 - Mark Pattison
1
只要你是真的,abs(first - second) < error(或者 <= error)更加简洁易懂。 - glglgl
3
在大多数情况下是正确的(并非全部情况),但并没有真正回答这个问题。 - milleniumbug
4
测试浮点数是否相等通常是有用的。但是,使用未经精心选择的epsilon进行比较是不明智的,而且在测试相等性时使用epsilon更加不明智。 - tmyklebu
1
如果你在浮点数键上对数组进行排序,我可以保证,如果你尝试使用比较浮点数的技巧,代码将无法正常工作。因为a == b和b == c意味着a == c的保证不再存在。对于哈希表,完全相同的问题也存在。当相等关系不是传递的时候,你的算法就会崩溃。 - gnasher729
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接