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


68

我的问题不是关于浮动精度。这是关于为什么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

该问题提供了有关浮点数和十进制类型之间差异的更多详细信息。
2013年

仅作记录,没有真正的答案:Math.Abs(.1d + .2d - .3d) < double.Epsilon这应该是更好的平等方法。
2013年

10
仅供参考==不是“参考”比较,也不.Equals()是“价值”比较。它们的实现是特定于类型的。
克里斯·辛克莱

15
需要澄清的是:区别在于,第一种情况0.1 + 0.2 == 0.3是一个常量表达式,可以在编译时完全计算出该常量表达式。在(0.1 + 0.2).Equals(0.3)0.1 + 0.20.3都是常量表达式,但平等是由运行时计算的,而不是由编译器。明白了吗?
埃里克·利珀特

8
另外,也要谨记:导致计算以更高的精度执行的差异不必是“环境的”。无论任何环境细节,无论出于何种原因,都允许编译器和运行时使用更高的精度。实际上,何时使用较高精度与较低精度的决定通常取决于寄存器的可用性。注册的表达式具有更高的精度。
埃里克·利珀特

Answers:


133

这个问题措辞混乱。让我们将其分解为许多较小的问题:

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

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

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向上取整...如果我们转到0.57142857的八个位置,则误差的大小要小得多,但方向相反;四舍五入。

因为更改精度可以更改每个单独计算中的误差是收益还是损失,所以这可以更改给定聚合计算的误差是相互补充还是相互抵消。最终结果是,有时精度较低的计算要比精度较高的计算更接近“真实”结果,因为在精度较低的计算中,您会很幸运,并且错误方向不同。

我们希望以更高的精度进行计算总会得到一个更接近于真实答案的答案,但是该论点反而表明了这一点。这就解释了为什么有时用浮点数进行计算会给出“正确”的答案,而使用双精度数(具有两倍的精度)却给出“错误”的答案,对吗?

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

如果float和double使用二进制数字,那么如何获得准确的十进制算术运算呢?

如果您需要精确的十进制数学,则使用decimal类型;它使用十进制分数,而不是二进制分数。您所付出的代价是它更大且更慢。当然,正如我们已经看到的那样,分数或三分之一之类的分数将无法准确表示。但是,实际上是十进制小数的任何小数都将以零误差表示,最多约29个有效数字。

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

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

允许编译器和运行时这样做,但是当时感觉很不错。它们不必在机器之间,每次运行之间保持一致,等等。由于这只能使计算更准确,因此不将其视为错误。这是一个功能。这项功能使编写具有可预测行为的程序变得异常困难,但这是一项功能。

因此,这意味着在编译时执行的计算(如字面量0.1 + 0.2)会比在运行时使用变量进行的相同计算产生不同的结果吗?

是的

比较0.1 + 0.2 == 0.3to的结果(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可以完全根据编译器的想法将其计算为true或false。它可以在星期二产生true,在星期四产生false,可以在一台机器上产生true,而在另一台机器上产生false,如果该表达式在同一程序中出现两次,则可以产生true和false。无论出于任何原因,此表达式都可以具有值;此处允许编译器完全不可靠。

正确。

通常将这种情况报告给C#编译器团队的方式是,某人具有一些表达式,这些表达式在调试时进行编译,而在发布模式下进行编译时则生成false。这是最常见的情况,因为调试和发布代码生成会更改寄存器分配方案。但是编译器允许做这种表达任何它喜欢的,只要它选择真或假。(例如,它不能产生编译时错误。)

这是疯狂。

正确。

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

不是我,那是肯定的。

英特尔决定制造一种浮点数学芯片,要获得一致的结果,该芯片要昂贵得多。编译器中关于要注册的操作与要保留在堆栈上的操作的微小选择可能会导致结果大相径庭。

如何确保结果一致?

decimal就像我之前说的那样使用类型。或以整数形式进行所有数学运算。

我必须使用双精度或浮点数;我可以做些什么来鼓励一致的结果吗?

是。如果将任何结果存储到任何静态字段,类型为float或double的类数组元素的任何实例字段中,则可以保证将其截断为32或64位精度。(这保证明确为加盟店当地人或正式的参数进行。)此外,如果你做一个运行时投来(float)(double)对已经是该类型的,则编译器会发出特殊的代码表达的是力量的结果截断,就好像它已分配给字段或数组元素。(不保证在编译时执行的转换(即对常量表达式进行转换)。)

为了阐明最后一点:C#语言规范是否提供这些保证?

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

语言规范在这个主题上必须说的是,浮点运算可以根据实现的自由度以更高的精度执行。


1
当我们分配布尔结果= 0.1f + 0.2f == 0.3f时,会发生问题。当我们不在变量中存储0.1f + 0.2f时,我们将得到false。如果我们将0.1f + 0.2f存储在变量中,则为true。它与通用浮点运算法则(如果有的话)几乎没有关系,主要的主要问题是为什么bool x = 0.1f + 0.2f == 0.3f为假,而float temp = 0.1f + 0.2f; bool x = temp == 0.3f是正确的,其余部分是通常的浮点问题部分
Valentin Kuzub 2013年

14
埃里克·利珀特(Eric Lippert)与我回答相同的问题时,我总是感到damn! my answer doesn't look logical anymore..
SonerGönül2013年

5
我真的很感激您仍然花时间并有耐心地为这样一个精心撰写且篇幅较长的帖子撰写文章,这个问题可能每周出现一次。+1
Groo

20
@MarkHurd:我认为您没有得到我在这里所说的一切的全部影响。这不是C#编译器或VB编译器做什么的问题。允许C#编译器出于任何原因随时为该问题提供答案。您可以编译同一程序两次,并获得不同的答案。您可以在同一程序中问两次问题,并得到两个不同的答案。C#和VB不会产生“相同的结果”,因为C#和C#不一定会产生相同的结果。如果它们碰巧产生相同的结果,那是一个幸运的巧合。
埃里克·利珀特

5
真是个答案。这就是为什么我使用StackOverflow。
西德·霍兰德

8

当你写

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

实际上,这些不完全是0.10.2并且0.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中有一个问题指向这样的问题,例如(.NET中的小数,浮点和双精度之间的区别?以及.NET中的浮点错误处理),但是我建议您阅读称为“酷”的文章。

What Every Computer Scientist Should Know About Floating-Point Arithmetic

好吧,嬉皮士所说的更合逻辑。真实的情况是在这里,完全以依赖compiler/computercpu

基于leppie代码,此代码可在我的Visual Studio 2010Linqpad上使用,结果是True/ False,但是当我在ideone.com尝试时,结果将是True/True

检查演示

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

浮点数与相等运算符的比较。四舍五入时可能会损失精度。

在此处输入图片说明


他正在询问单精度情况。双精度表壳没有问题。
leppie

1
显然,将要执行的代码与编译器之间也存在差异。0.1f+0.2f==0.3f在调试和发布模式下都将被编译为false。因此,对于平等经营者来说将是错误的。
Caramiriel

6

如评论中所述,这是由于编译器不断进行传播并以更高的精度执行计算(我相信这取决于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==.3ffalseIL中一样发出的信号,因此编译器在编译时进行了计算。

确认恒定的折叠/传播编译器优化

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

但是为什么在最后一种情况下它不做相同的优化呢?
Groo

2
@SonerGönül:很快就会被他的殿下所掩盖; p谢谢
leppie

好的,让我更清楚地陈述一下,正如我指的是OP的最后一种情况:但是为什么在这种Equals情况下它不做相同的优化呢?
Groo

@Groo:如果你是说(0.1d+.2d).Equals(.3d) == false,因为是!
leppie

1
@ njzk2:很好,floatstruct,因此不能被子类化。浮点常量也有一个相当恒定的Equals实现。
Groo

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)));
}

我同意您的最后声明:)
leppie

@leppie用新测试更新了我的答案。你能告诉我为什么第一次通过而第二次没有通过。考虑到实施Equals,我不太明白
Valentin Kuzub

0

== 关于比较精确的浮点值。

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


检查我对float Equals实现的回答。实际的区别是,equals是在运行时执行的,而==可以在编译时执行,==也是“布尔方法”(我听说过布尔函数的更多信息),实际上
Valentin Kuzub

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
    }
}
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.