是否可以通过减去两个不相等的浮点数来获得0?


131

在下面的示例中是否可以除以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
@Thirler听起来像是时候对我使用JUnit Testing!
马特·克拉克

7
@bluebrain,我的猜测是您的文字数字2.000等包含很多小数,用浮点数表示。因此,在比较中,最后一个将不会由实际使用的数字表示。
瑟勒

4
@蒂勒。“您不能真正保证分配给浮点数或双精度数的数字是准确的”
guness

4
请注意,在这种情况下返回0可能会导致难以调试的歧义,因此请确保您确实要返回0,而不是引发异常或返回NaN。
m0skit0 2015年

Answers:


132

在Java中,a - b永远不等于0if a != b。这是因为Java要求IEEE 754浮点运算支持不规范的数字。从规格

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

如果FPU使用非正规数,则减去不等数永远不会产生零(与乘法不同),另请参见此问题

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

也就是说,表达式可能2 / (a - b)会溢出,例如使用a = 5e-308b = 4e-308


4
但是OP想要知道2 /(ab)。可以保证这是有限的吗?
塔伊米尔2015年

感谢您的回答,我添加了一个指向Wikipedia的链接,以解释非正规数字。
瑟勒(Thirler)2015年

3
@Taemyr看到我的编辑。该划分实际上可能溢出。
nwellnhof

@Taemyr (a,b) = (3,1)=> 2/(a-b) = 2/(3-1) = 2/2 = 1我不知道IEEE浮点运算是否正确
Cole Johnson

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
避免该问题并立即简化测试。我喜欢
2015年

11
-1如果不a=b,你不应该返回00在IEEE 754中进行除法可以使您无穷无尽,这也不例外。您正在避免该问题,因此返回0是一个等待发生的错误。考虑一下1/x + 1。如果为x=0,则结果为1,而不是正确的值:无穷大。
科尔·约翰逊

5
@ColeJohnson正确答案也不是无穷大(除非您指定限制来自哪一侧,右侧= + inf,左侧= -inf,未指定=未定义或NaN)。
尼克T

12
@ChrisHayes:这是对问题的有效答案,认识到该问题可能是XY问题:meta.stackexchange.com/questions/66377/what-is-the-xy-problem
slebetman 2015年

17
@ColeJohnson返回0并不是真正的问题。这就是OP在问题中所做的。您可以在该块的那部分放置一个例外或任何适合该情况的东西。如果您不喜欢返回0,那应该是对该问题的批评。当然,像OP那样做并不能保证答案是正确的。给定功能完成后,该问题与进一步的计算无关。众所周知,该程序的要求必须返回0
jpmc26 2015年

25

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

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

编辑:

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

  1. 假“本身不是数字比较”为假,但将具有相同的位模式。

  2. -0.0被定义为将true与+0.0进行比较,并且它们的位模式不同。

因此,如果aand和bare Double.NaN,则将到达else子句,但由于NaN - NaN还返回NaN,所以不会被零除。


11
伊朗 并非完全正确。假“本身不是数字比较”为假,但将具有相同的位模式。还定义了-0.0以将true与+0.0进行比较,并且它们的位模式不同。
Bathsheba 2015年

1
@Bathsheba我没有考虑这些特殊情况。感谢您的评论。
伊兰2015年

2
@Eran,很好的一点是,除以0将在浮点数中返回无穷大。将其添加到问题。
瑟勒2015年

2
@Prashant,但在这种情况下不会进行除法,因为a == b将返回true。
伊兰2015年

3
实际上,您可能会得到FP零除的例外,这是IEEE-754标准定义的一个选项,尽管这可能不是大多数人对“ exception”的意思;)
Voo 2015年

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)。这意味着结果适用于所有可能的舍入模式(其中有五种)。结果还包括游戏中任何变量可能为NaNor无限的可能性。

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的链接和在线解释器的链接?虽然这个答案似乎是完全合法的,但有人可以认为这些结果是错误的。
2015年

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

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


6

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

有效处理非规范化浮点格式的电路成本很高,而且并非所有处理器都包含该格式。有些处理器都提供了非常小的数字既可以采用具有操作之间的选择是很多比其他值的操作速度较慢,或具有处理器简单地认为这是标准化的格式零过小的数。

Java规范暗示实现应该支持非规范化格式,即使在这样的机器上,这样做也会使代码运行得更慢。另一方面,某些实现可能会提供一些选项,以允许代码更快地运行,以换取对值稍作草率的处理,这在大多数情况下将变得无关紧要(在值太小而又无关紧要的情况下,进行计算所需的时间是重要的十倍,这可能会很烦人,因此在许多实际情况下,“清零”比“慢而精确”的算法有用。


6

在IEEE 754之前的较早时期,a!= b并不意味着ab!= 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。


2

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

浮点数或多或少以科学计数法存储-也就是说,与看到35.2相比,所存储的数字更像是3.52e2。

为了方便起见,请想象一下,我们有一个浮点数单元,以10为基数操作,精度为3位。如果从10.0中减去9.99,会发生什么?

1.00e2-9.99e1

移位以赋予每个值相同的指数

1.00e2-0.999e2

四舍五入至三位数

1.00e2-1.00e2

哦!

最终能否实现取决于FPU设计。由于双精度指数的取值范围非常大,因此硬件必须在某个时候进行内部取整,但是在上述情况下,内部仅增加1位数字就可以避免任何问题。


1
处理这种情况时,要求保持对齐的操作数以进行减法的寄存器需要保留额外的两位(称为“保护位”)。在减法会导致从最高有效位借位的情况下,较小的操作数的大小必须超过较大操作数的大小的一半(这意味着它只能有一个额外的精度),否则结果必须至少为较小操作数大小的一半(意味着只需要再增加一位,再加上足以确保正确舍入的信息)。
supercat

1
“是否会最终发生取决于FPU设计。”不,因为Java定义认为不可能,所以不会发生。FPU设计与它无关。
Pascal Cuoq 2015年

@PascalCuoq:如果我错了,请纠正我,但未strictfp启用,计算可能会产生太小的值,double但适合扩展精度浮点值。
supercat

@supercat的缺失strictfp仅影响“中间结果”的值,我引用docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.4a并且bdouble变量,而不是中间结果,因此它们的值是双精度值,因此是2 ^ -1074的倍数。因此,将这两个双精度值相减是2 ^ -1074的倍数,因此,较宽的指数范围确实会更改差值为0且a == b的性质。
Pascal Cuoq 2015年

@supercat这很有意义-您只需要多一点即可完成此操作。
Keldor314

1

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

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

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

6
“永远不要”有点强,但这通常是一个很好的建议。
马克·帕蒂森

1
当您是对的时,abs(first - second) < error(或<= error)更容易,更简洁。
glglgl 2015年

3
虽然在大多数情况下(并非全部)都是正确的,但并不能真正回答问题。
milleniumbug

4
测试浮点数是否相等通常很有用。与未经仔细选择的epsilon进行比较没有理智,而在测试相等性时与epsilon进行比较则没有理智。
tmyklebu 2015年

1
如果对浮点键对数组进行排序,那么当您尝试使用将浮点数与epsilon进行比较的技巧时,我可以保证您的代码将不起作用。因为保证a == b和b == c意味着a == c不再存在。对于哈希表,完全相同的问题。当相等性不能传递时,您的算法就会中断。
gnasher729 2015年

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

核心问题是,当您使用“太多”的十进制数时,例如,当您处理不能以数值形式写入的double时,double的计算机表示形式(aka浮点数或数学语言中的实数)是错误的。 pi或1/3的结果)。

所以a == b不能用a和b的任何双精度值完成,当a = 0.333和b = 1/3时如何处理a == b?取决于您的操作系统,FPU,数字,语言还是0后的3,您将得到true或false。

无论如何,如果您在计算机上执行“双值计算”,则必须处理准确性,因此a==b必须要做的是absolute_value(a-b)<epsilon,而不是做,并且epsilon与您当时在算法中建模的相对。您不能为所有双重比较都提供一个epsilon值。

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

PS:哼,我在这里回答的一切或多或少都来自其他人的回应和评论。


1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.