提问人:LZW 提问时间:2/28/2013 最后编辑:Daniel A.A. PelsmaekerLZW 更新时间:10/7/2019 访问量:7632
(.1f+.2f==.3f) != (.1f+.2f)。等于(.3f) 为什么?
(.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) Why?
问:
我的问题不是关于浮动精度。这是关于为什么与 不同。Equals()
==
我明白为什么是(而是)。
我明白这是参考,是价值比较。(编辑:我知道还有更多。.1f + .2f == .3f
false
.1m + .2m == .3m
true
==
.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
答:
当你写
double a = 0.1d;
double b = 0.2d;
double c = 0.3d;
实际上,这些并不完全是 ,并且 .来自 IL 代码;0.1
0.2
0.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 说的更合乎逻辑。真实情况就在这里,完全取决于/或.compiler
computer
cpu
基于 leppie 代码,此代码适用于我的 Visual Studio 2010 和 Linqpad,结果是 /,但是当我在 ideone.com 上尝试时,结果将是True
False
True
/True
检查 DEMO。
提示:当我写 Resharper 时警告我;Console.WriteLine(.1f + .2f == .3f);
浮点数与相等运算符的比较。可能 舍入值时精度损失。
评论
0.1f+0.2f==0.3f
正如评论中所说,这是由于编译器执行恒定传播并以更高的精度执行计算(我相信这取决于 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==.3f
false
确认常量折叠/传播编译器优化
const float f1 = .1f + .2f;
const float f2 = .3f;
Console.WriteLine(f1 == f2); // prints false
评论
Equals
案例中不做同样的优化呢?
(0.1d+.2d).Equals(.3d) == false
float
struct
Equals
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)));
}
评论
这个问题的措辞令人困惑。让我们把它分解成许多小问题:
为什么在浮点运算中十分之一加十分之二并不总是等于十分之三?
让我给你打个比方。假设我们有一个数学系统,其中所有数字都四舍五入到小数点后五位。假设你说:
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 实现具有回归测试,可确保编译器的每个新版本都具有此行为。
关于这个主题,语言规范必须说的是,浮点运算可以由实现的判断以更高的精度执行。
评论
damn! my answer doesn't look logical anymore..
==
是关于比较确切的浮点值。
Equals
是一个布尔方法,可能返回 true 或 false。具体实现可能会有所不同。
评论
我不知道为什么,但目前我的一些结果与你的不同。请注意,第三个和第四个测试恰好与问题相反,因此您的部分解释现在可能是错误的。
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
}
}
评论
Math.Abs(.1d + .2d - .3d) < double.Epsilon
==
.Equals()
0.1 + 0.2 == 0.3
(0.1 + 0.2).Equals(0.3)
0.1 + 0.2
0.3