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

(.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) Why?

提问人:LZW 提问时间:2/28/2013 最后编辑:Daniel A.A. PelsmaekerLZW 更新时间:10/7/2019 访问量:7632

问:

我的问题不是关于浮动精度。这是关于为什么与 不同。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
C# 相等 浮点精度

评论

0赞 Amicable 2/28/2013
本问题详细介绍了浮点型和十进制型之间的差异。
0赞 Malmi 2/28/2013
只是为了记录,没有真正的答案:这应该是更好的平等方法。Math.Abs(.1d + .2d - .3d) < double.Epsilon
11赞 Chris Sinclair 2/28/2013
仅供参考不是“参考”比较,也不是“价值”比较。它们的实现是特定于类型的。==.Equals()
15赞 Eric Lippert 2/28/2013
澄清一下:区别在于,在第一种情况下,这是一个常量表达式,可以在编译时完全计算。和 都是常量表达式,但相等性是由运行时计算的,而不是由编译器计算的。清楚吗?0.1 + 0.2 == 0.3(0.1 + 0.2).Equals(0.3)0.1 + 0.20.3
8赞 Eric Lippert 2/28/2013
此外,需要挑剔的是:导致以更高精度执行计算的差异不一定是“环境”的;编译器和运行时都允许出于任何原因使用更高的精度,而不管任何环境细节如何。实际上,何时使用更高精度还是更低精度的决定实际上通常取决于寄存器的可用性;注册的表达式精度更高。

答:

8赞 Soner Gönül 2/28/2013 #1

当你写

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

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

  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

好吧,leppie 的更合乎逻辑。真实情况就在这里,完全取决于/或.compilercomputercpu

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

检查 DEMO。

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

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

enter image description here

评论

0赞 leppie 2/28/2013
他问的是单精度表壳。双精度外壳没有问题。
1赞 Caramiriel 2/28/2013
显然,将要执行的代码和编译器之间也存在差异。 将在调试和发布模式下编译为 false。因此,对于相等运算符来说,它将是错误的。0.1f+0.2f==0.3f
5赞 leppie 2/28/2013 #2

正如评论中所说,这是由于编译器执行恒定传播并以更高的精度执行计算(我相信这取决于 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还指出,在 IL 中发出,因此编译器在编译时进行了计算。.1f+.2f==.3ffalse

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

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

评论

0赞 vgru 2/28/2013
但是,为什么在最后一种情况下它不进行相同的优化呢?
2赞 leppie 2/28/2013
@SonerGönül:很快就要被;p殿下黯然失色了谢谢
0赞 vgru 2/28/2013
好的,让我更清楚地说明一下,因为我指的是 OP 的最后一个案例:但是为什么它在 Equals 案例中不做同样的优化呢?
0赞 leppie 2/28/2013
@Groo:如果你的意思是,因为它是!(0.1d+.2d).Equals(.3d) == false
1赞 vgru 2/28/2013
@njzk2:嗯,是一个 ,所以它不能被子类化。浮点常量也有一个相当常量的实现。floatstructEquals
2赞 Valentin Kuzub 2/28/2013 #3

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

评论

0赞 leppie 2/28/2013
我同意你最后的说法:)
0赞 Valentin Kuzub 2/28/2013
@leppie用新的测试更新了我的答案。你能告诉我为什么第一次通过而第二次没有。我不太明白,鉴于 Equals 实现
136赞 Eric Lippert 2/28/2013 #4

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

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

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

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 使用二进制数字,我怎样才能获得准确的十进制算术计算呢?

如果您需要准确的十进制数学,请使用类型;它使用十进制分数,而不是二进制分数。你付出的代价是它更大、更慢。当然,正如我们已经看到的,像三分之一或七分之四这样的分数不会被准确表示。然而,任何实际上是十进制分数的分数都将以零误差表示,最多约 29 位有效数字。decimal

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

不,您不能保证浮动或双打。编译器和运行时都允许以比规范要求的精度更高的精度执行浮点计算。特别是,允许编译器和运行时以 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# 编译器团队报告的方式是,有人有一些表达式在调试中编译时产生 true,在发布模式下编译时产生 false。这是最常见的情况,因为调试和发布代码生成会更改寄存器分配方案。但是编译器可以对这个表达式做任何它喜欢的事情,只要它选择 true 或 false。(例如,它不会产生编译时错误。

这是疯狂的。

正确。

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

不是我,这是肯定的。

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

如何确保结果的一致性?

正如我之前所说,使用类型。或者用整数做你所有的数学运算。decimal

我必须使用双打或浮点;我能做些什么来鼓励一致的结果吗?

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

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

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

关于这个主题,语言规范必须说的是,浮点运算可以由实现的判断以更高的精度执行。

评论

1赞 Valentin Kuzub 2/28/2013
当我们分配 bool result= 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 为 true,rest 是通常的浮点问题部分
16赞 Soner Gönül 2/28/2013
埃里克·利珀特(Eric Lippert)和我回答同样的问题时,我总觉得damn! my answer doesn't look logical anymore..
5赞 vgru 2/28/2013
我真的很感谢你仍然花时间和耐心为一个可能每周出现一次的问题贡献如此精心撰写和相当长的帖子。+1
21赞 Eric Lippert 2/28/2013
@MarkHurd:我认为你没有得到我在这里所说的全部影响。这不是 C# 编译器或 VB 编译器做什么的问题。允许 C# 编译器在任何时候出于任何原因给出该问题的任一答案。您可以将同一个程序编译两次并得到不同的答案。您可以在同一个程序中提出两次问题,并得到两个不同的答案。C# 和 VB 不会产生“相同的结果”,因为 C# 和 C# 不一定产生相同的结果。如果它们碰巧产生相同的结果,那将是一个幸运的巧合。
5赞 Sid Holland 3/6/2013
多么好的答案。这就是我使用 StackOverflow 的原因。
0赞 njzk2 2/28/2013 #5

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

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

评论

0赞 Valentin Kuzub 2/28/2013
检查我的答案 float Equals 实现。实际的区别在于 equals 是在运行时执行的,而 == 可以在编译时执行,== 实际上也是一个“布尔方法”(我听说过更多关于布尔函数的信息)
0赞 imba-tjd 10/7/2019 #6

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

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