【问题标题】:(.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) Why?(.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) 为什么?
【发布时间】:2013-02-13 13:14:57
【问题描述】:

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

【问题讨论】:

  • This question 提供了有关浮点类型和十进制类型之间差异的更多详细信息。
  • 只是为了记录,没有真正的答案:Math.Abs(.1d + .2d - .3d) < double.Epsilon这应该是更好的平等方法。
  • 仅供参考 ==不是“参考”比较,.Equals()不是“价值”比较。它们的实现是特定于类型的。
  • 澄清一下:区别在于第一种情况0.1 + 0.2 == 0.3 是一个常量表达式,可以在编译时完全计算。在(0.1 + 0.2).Equals(0.3) 中,0.1 + 0.20.3 都是常量表达式,但相等性由运行时计算,而不是由编译器计算。清楚吗?
  • 另外,只是为了挑剔:导致计算以更高精度执行的差异不必是“环境的”;无论任何环境细节如何,编译器和运行时都可以出于任何原因使用更高的精度。实际上,何时使用更高精度还是更低精度的决定实际上通常取决于寄存器的可用性。注册的表达式精度更高。

标签: c# equality floating-accuracy


【解决方案1】:

当你写作时

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 2010Linqpad,结果是 True/False,但是当我尝试使用它时ideone.com,结果将是True/True

检查DEMO

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

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

【讨论】:

  • 他在询问单精度情况。双精度大小写没有问题。
  • 显然,将要执行的代码和编译器之间也存在差异。 0.1f+0.2f==0.3f 在调试和发布模式下都将编译为 false。因此,相等运算符将是错误的。
【解决方案2】:

正如 cmets 中所说,这是由于编译器进行不断传播并以更高的精度执行计算(我相信这取决于 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

【讨论】:

  • 但是为什么在最后一种情况下不做同样的优化呢?
  • @SonerGönül:很快就会被殿下黯然失色;p 谢谢
  • 好的,让我说得更清楚一点,因为我指的是 OP 的最后一个案例:但是为什么它不在 Equals 案例中做同样的优化呢?
  • @Groo:如果你的意思是(0.1d+.2d).Equals(.3d) == false,因为它是!
  • @njzk2:嗯,float 是一个struct,所以它不能被子类化。浮点常量也有一个相当恒定的Equals 实现。
【解决方案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)));
}

【讨论】:

  • 我同意你最后的说法:)
  • @leppie 用新测试更新了我的答案。你能告诉我为什么第一次通过而第二次没有。我不太明白,考虑到 Equals 的实现
【解决方案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 也不能用任何有限的二进制数字准确表示。有的向上取整,有的向下取整,添加是增加误差还是消除误差取决于具体细节每个系统中有多少个二进制数字。也就是说,precision 的变化可以改变 answer 的好坏。一般来说,精度越高,答案越接近真实答案,但并非总是如此。

如果 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# 编译器团队报告这种情况的方式是,有人有一些表达式,当他们在调试模式下编译时生成 true,而在发布模式下编译时生成 false。这是最常见的情况,因为调试和发布代码生成更改了寄存器分配方案。但是编译器允许对这个表达式做任何它喜欢的事情,只要它选择真或假。 (比如说,它不会产生编译时错误。)

这太疯狂了。

正确。

这个烂摊子我该怪谁?

不是我,那是肯定的。

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

如何确保结果一致?

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

我必须使用双精度或浮点数;我可以做任何事情来鼓励一致的结果吗?

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

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

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

语言规范对此主题的全部内容是浮点运算可以根据实现的判断以更高的精度执行。

【讨论】:

  • 当我们分配 bool result= 0.1f+0.2f==0.3f 时会出现问题。当我们不将 0.1f+0.2f 存储在变量中时,我们会得到错误。如果我们将 0.1f+0.2f 存储在变量中,我们会得到 true。如果有的话,它与一般的浮点运算几乎没有关系,这里的主要问题是为什么 bool x=0.1f+0.2f==0.3f 是假的,但是 float temp=0.1f+0.2f; bool x=temp==0.3f 为真,其余为常用浮点题部分
  • Eric Lippert和我回答同样的问题时,我总觉得damn! my answer doesn't look logical anymore..
  • 我真的很感激你仍然花时间和耐心为一个可能每周出现一次的问题提供如此精心编写和相当长的帖子。 +1
  • @MarkHurd:我认为你没有得到我在这里所说的全部影响。这不是 C# 编译器或 VB 编译器做什么的问题。允许 C# 编译器在任何时间出于任何原因对该问题给出任一答案。你可以将同一个程序编译两次,得到不同的答案。您可以在同一个程序中两次询问该问题并得到两个不同的答案。 C# 和 VB 不会产生“相同的结果”,因为 C# 和 C# 不一定会产生相同的结果。如果他们碰巧产生了相同的结果,那是一个幸运的巧合。
  • 多么好的答案。这就是我使用 StackOverflow 的原因。
【解决方案5】:

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

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

【讨论】:

  • 检查我对 float Equals 实现的回答。实际区别是equals在运行时执行,而==可以在编译时执行,==也是一种“布尔方法”(我听说过更多关于布尔函数),实际上
【解决方案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
    }
}

【讨论】:

    猜你喜欢
    • 2010-10-16
    • 1970-01-01
    • 2020-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-16
    相关资源
    最近更新 更多