(.1f + .2f ==。3f)!=(。1f + .2f).Equals(.3f)为什么?

时间:2013-02-27 16:21:36

标签: c# equality floating-accuracy

关于浮动精度我的问题是。这是为什么Equals()==不同。

我理解为什么.1f + .2f == .3ffalse.1m + .2m == .3mtrue)。
我知道==是引用,.Equals()是值比较。 (编辑:我知道还有更多内容。)

但为什么(.1f + .2f).Equals(.3f) true,而(.1d+.2d).Equals(.3d)仍为false

 .1f + .2f == .3f;              // false
(.1f + .2f).Equals(.3f);        // true
(.1d + .2d).Equals(.3d);        // false

6 个答案:

答案 0 :(得分:131)

这个问题令人困惑。让我们把它分解成许多小问题:

  

为什么十分之一加十分之二在浮点运算中并不总是等于十分之三?

让我给你一个类比。假设我们有一个数学系统,其中所有数字都四舍五入到五位小数。假设你说:

x = 1.00000 / 3.00000;

你会期望x为0.33333,对吗?因为这是我们系统中与真实答案的最接近的号码。现在假设你说

y = 2.00000 / 3.00000;

您希望y为0.66667,对吧?因为再一次,这是我们系统中最接近真实答案。 0.66666 更远来自0.66667的三分之二。

请注意,在第一种情况下,我们向下舍入,在第二种情况下,我们向上舍入。

现在我们说

q = x + x + x + x;
r = y + x + x;
s = y + y;

我们得到了什么?如果我们做了精确的算术,那么这些中的每一个显然都是三分之二,并且它们都是相等的。但他们不平等。尽管1.33333是我们系统中最接近三分之二的数字,但只有r具有该值。

q是1.33332 - 因为x有点小,每增加一个误差,最终结果就太小了。同样,s太大了;它是1.33334,因为y有点太大了。 r得到了正确的答案,因为y的太大,被x的太小的结果所取消,结果得到了正确的结果。

  

精确位置的数量是否会影响误差的大小和方向?

是;更精确使得误差的幅度更小,但是可以改变计算是否由于误差而产生损失或增益。例如:

b = 4.00000 / 7.00000;

b将是0.57143,它从0.571428571的真实值向上舍入...我们是否已经到了8个位置,即0.57142857,其误差幅度远远小于相反方向;它向下舍入。

因为改变精度可以改变每个计算中的错误是增益还是损失,这可以改变给定的聚合计算错误是相互加强还是相互抵消。最终结果是,有时低精度计算更接近" true"结果比计算精度更高,因为在精度较低的计算中,你很幸运,错误的方向不同。

  

我们希望以更高的精度进行计算总能给出更接近真实答案的答案,但这个论点显示了其他方面。这就解释了为什么有时浮点计算会给出正确的"回答,但双精度计算 - 精度的两倍 - 给出了错误"回答,对吗?

是的,这正是您的示例中正在发生的事情,除了小数精度的五位数而不是五位数的精度。 binary 精度。正如三分之一不能准确地表示为五位或任何有限数字的十进制数字,0.1,0.2和0.3不能精确地表示为任何有限数量的二进制数字。其中一些将被四舍五入,其中一些将向下舍入,以及是否添加增加错误或取消错误取决于具体细节每个系统中有多少个二进制数字。也就是说,精度的更改可以改变答案,无论好坏。通常,精度越高,答案越接近真实答案,但并非总是如此。

  

如果float和double使用二进制数字,我怎样才能得到准确的十进制算术计算?

如果您需要精确的十进制数学运算,请使用decimal类型;它使用小数部分,而不是二进制分数。你支付的价格是相当大和慢。当然,正如我们已经看到的那样,三分之一或四分之七的分数无法准确表示。然而,实际上是小数部分的任何分数都将用零误差表示,最多约29个有效数字。

  

好的,我接受所有浮点方案由于表示错误而引入不准确性,并且这些不准确性有时会根据计算中使用的精度位数累加或相互抵消。我们是否至少保证这些不准确性一致

不,你没有浮动或双打的保证。允许编译器和运行时以更高精度执行浮点计算,而不是规范要求。特别是,允许​​编译器和运行时以64位或80位或128位进行单精度(32位)算术,或者任何大于32()的位数。

允许编译器和运行时然而他们当时感觉。它们不需要在机器之间,从运行到运行等方面保持一致。由于这只能使计算更准确,因此不会将其视为错误。这是一个功能。这个功能使得编写可预测的程序非常困难,但仍然是一个功能。

  

这意味着在编译时执行的计算(如文字0.1 + 0.2)可以提供与运行时使用变量执行的相同计算不同的结果吗?

是的。

  

如何比较0.1 + 0.2 == 0.3(0.1 + 0.2).Equals(0.3)的结果?

由于第一个是由编译器计算的,第二个是由运行时计算的,我只是说他们可以随心所欲地使用比规范要求更高的精度,是的,那些可以给出不同的结果。也许其中一个选择仅以64位精度进行计算,而另一个选择80位或128位精度进行部分或全部计算并获得差异答案。

  

所以在这里等一下。您不仅要说0.1 + 0.2 == 0.3可能与(0.1 + 0.2).Equals(0.3)不同。您可以说,0.1 + 0.2 == 0.3可以完全按照编译器的方式计算为真或假。它可以在星期二产生真假,在星期四产生假,它可以在一台机器上产生真假而在另一台机器上产生假,如果表达式在同一程序中出现两次,它可能产生真和假。该表达式可以出于任何原因具有任何值;这里允许编译器完全不可靠。

正确。

通常向C#编译器团队报告的方式是某人有一些表达式,当他们在debug中编译时生成true,而在release模式下编译时生成false。这是最常见的情况,因为调试和释放代码生成会更改寄存器分配方案。但编译器允许使用此表达式执行任何喜欢的操作,只要它选择true或false即可。 (它不能产生编译时错误。)

  

这很疯狂。

正确。

  

我应该为这个烂摊子责怪谁?

不是我,那是肯定的。

英特尔决定制作浮点数学芯片,其中制作一致结果的成本远远高得多。编译器中关于要注册的操作与要保留在堆栈上的操作的小选择可能会导致结果的巨大差异。

  

如何确保结果一致?

使用decimal类型,如前所述。或者用整数做所有的数学运算。

  

我必须使用双打或花车;我可以任何来鼓励一致的结果吗?

是。如果将任何结果存储到任何静态字段中,则类型为float或double的类或数组元素的任何实例字段,则保证被截断回32位或64位精度。 (这种保证明确地为商店提供给本地人或正式参数。)此外,如果您执行运行时强制转换为(float)(double)一个已经是该类型的表达式,然后编译器将发出特殊代码,强制结果被截断,就好像它被分配给一个字段或数组元素一样。 (在编译时执行的转换 - 即对常量表达式进行强制转换 - 不保证这样做。)

  

澄清最后一点:C#语言规范是否做出了这些保证?

没有。 运行时保证存储到数组或字段截断中。 C#规范不保证标识转换会截断但Microsoft实现具有回归测试,以确保编译器的每个新版本都具有此行为。

所有语言规范必须说明的是浮点运算可以由实现自行决定以更高的精度执行。

答案 1 :(得分:8)

写作时

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;

实际上,这些并不完全是0.10.20.3。来自IL代码;

  IL_0001:  ldc.r8     0.10000000000000001
  IL_000a:  stloc.0
  IL_000b:  ldc.r8     0.20000000000000001
  IL_0014:  stloc.1
  IL_0015:  ldc.r8     0.29999999999999999

在SO中有一个问题,指出像(Difference between decimal, float and double in .NET?Dealing with floating point errors in .NET这样的问题,但我建议你阅读一篇很酷的文章;

What Every Computer Scientist Should Know About Floating-Point Arithmetic

,leppie said更符合逻辑。真实情况在这里,完全依赖 compiler / computercpu

基于leppie代码,此代码适用于我的 Visual Studio 2010 Linqpad ,因此True / False,但是我在ideone.com上尝试了它,结果将是True / True

检查 DEMO

提示:当我写Console.WriteLine(.1f + .2f == .3f); Resharper警告我时;

  

使用相等运算符比较浮点数。可能   在舍入值时精度下降。

enter image description here

答案 2 :(得分:5)

正如评论中所说,这是由于编译器进行持续传播并以更高的精度执行计算(我相信这取决于CPU)。

  var f1 = .1f + .2f;
  var f2 = .3f;
  Console.WriteLine(f1 == f2); // prints true (same as Equals)
  Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)

@Caramiriel还指出.1f+.2f==.3f在IL中作为false发出,因此编译器在编译时进行了计算。

确认常量折叠/传播编译器优化

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false

答案 3 :(得分:2)

测试通过后的FWIW

float x = 0.1f + 0.2f;
float result = 0.3f;
bool isTrue = x.Equals(result);
bool isTrue2 = x == result;
Assert.IsTrue(isTrue);
Assert.IsTrue(isTrue2);

问题实际上是这一行

  

0.1f + 0.2f == 0.3f

如上所述可能是编译器/ pc特定的

到目前为止,大多数人都是从错误的角度跳过这个问题

<强>更新

我认为另一个好奇的测试

const float f1 = .1f + .2f;
const float f2 = .3f;
Assert.AreEqual(f1, f2); passes
Assert.IsTrue(f1==f2); doesnt pass

单一等同实现:

public bool Equals(float obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}

答案 4 :(得分:0)

==是关于比较确切的浮点值。

Equals是一个布尔方法,可能返回true或false。具体实施可能会有所不同。

答案 5 :(得分:0)

我不知道为什么,但是目前我的一些结果与您的不同。请注意,第三和第四测试恰好与问题相反,因此您的部分解释现在可能是错误的。

using System;

class Test
{
    static void Main()
    {
        float a = .1f + .2f;
        float b = .3f;
        Console.WriteLine(a == b);                 // true
        Console.WriteLine(a.Equals(b));            // true
        Console.WriteLine(.1f + .2f == .3f);       // true
        Console.WriteLine((1f + .2f).Equals(.3f)); //false
        Console.WriteLine(.1d + .2d == .3d);       //false
        Console.WriteLine((1d + .2d).Equals(.3d)); //false
    }
}