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

LZW*_*LZW 67 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
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 132

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

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

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

x = 1.00000 / 3.00000;
Run Code Online (Sandbox Code Playgroud)

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

y = 2.00000 / 3.00000;
Run Code Online (Sandbox Code Playgroud)

你希望y是0.66667,对吗?因为,这是我们系统中与真实答案最接近的数字.0.66666 比0.66667 更远离三分之二.

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

现在,当我们说

q = x + x + x + x;
r = y + x + x;
s = y + y;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

b = 4.00000 / 7.00000;
Run Code Online (Sandbox Code Playgroud)

b将是0.57143,它从0.571428571的真实值向上舍入......如果我们去了8个位置,即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.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实现具有回归测试,以确保编译器的每个新版本都具有此行为.

关于该主题的所有语言规范都必须说明,浮点运算可以由实现自行决定以更高的精度执行.

  • @MarkHurd:我认为你没有得到我在这里所说的全部影响.这不是C#编译器或VB编译器的作用.允许C#编译器在任何时候出于任何原因**回答*该问题*.您可以编译两次相同的程序并获得不同的答案.您可以在同一个程序中两次提出问题并获得两个不同的答案.C#和VB不会产生"相同的结果",因为C#*和C#*不一定会产生相同的结果.如果他们碰巧产生相同的结果,那是一个幸运的巧合. (20认同)
  • 当_Eric Lippert_和我回答同样的问题时,我总觉得"该死的!我的回答看起来不合逻辑了 (14认同)
  • 我真的很感激你仍然花时间和耐心为这样一个经过精心编写和相当冗长的帖子做出贡献,这个问题可能每周都会出现一次.+1 (5认同)
  • 真是个答案.这就是我使用StackOverflow的原因. (5认同)

Son*_*nül 8

当你写作

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;
Run Code Online (Sandbox Code Playgroud)

其实,这些都是不完全0.1,0.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
Run Code Online (Sandbox Code Playgroud)

在SO中有一个问题,指出像(.NET中的十进制,浮点数和双精度之间的差异?以及处理.NET中的浮点错误)但我建议您阅读一篇名为的文章;

What Every Computer Scientist Should Know About Floating-Point Arithmetic

那么,leppie 的更合乎逻辑.真实的情况是在这里,完全以依赖compiler/ computercpu.

基于leppie代码,这个代码工作在我的Visual Studio 2010Linqpad,结果True/ False,但是当我试图在ideone.com,其结果将是True/True

检查演示.

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

浮点数与等式算子的比较.舍入值时可能会损失精度.

在此输入图像描述


lep*_*pie 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)
Run Code Online (Sandbox Code Playgroud)

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

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

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false
Run Code Online (Sandbox Code Playgroud)

  • @SonerGönül:很快就会被他的殿下所掩盖; p谢谢 (2认同)