如何检测Double类型的总精度丢失?

7
当我运行以下代码时,两行都会打印出0:
Double a = 9.88131291682493E-324;
Double b = a*0.1D;
Console.WriteLine(b);
Console.WriteLine(BitConverter.DoubleToInt64Bits(b));

如果操作结果超出范围,我希望能够得到Double.NaN。但实际上得到了0。似乎要检查以下内容才能检测到这种情况:
- 在操作之前,检查任何一个操作数是否为零。 - 在操作之后,如果两个操作数都不为零,则检查结果是否为零。如果不是,则让它运行。如果为零,则将Double.NaN分配给它,以表明它不是真正的零,而是无法在此变量中表示的结果。
这样做相当繁琐。有更好的方法吗?Double.NaN是用于什么的?我认为某些操作必须返回它,设计者肯定不是为了万一才放置它的。这可能是BCL中的一个错误吗?(我知道这不太可能,但这就是为什么我想了解Double.NaN应该如何工作的原因)
更新:
顺便说一下,这个问题不仅针对double,decimal也同样存在。
Decimal a = 0.0000000000000000000000000001m;
Decimal b =  a* 0.1m;
Console.WriteLine(b);

这也会得到零。

在我的情况下,我需要双精度浮点数,因为我需要它们提供的范围(我正在进行概率计算),而且我并不太担心精度问题。

然而,我需要能够检测到我的结果何时停止有意义,也就是当计算将值降低到无法用双精度浮点数表示时。

有没有实用的方法来检测这个问题?


但它实际上并不是NaN,只是比你可以表示的零更接近。 - SimpleVar
如果您需要检测精度损失,因为您不能有精度丢失,请使用Decimal或等效整数类型。 - Adam Houldsworth
1
NaN不用于下溢。它用于诸如负数的平方根之类的东西。 - Matthew Watson
1
@AdamHouldsworth,使用Decimal也会遇到同样的问题。实际上,对于这个例子来说,情况甚至更糟,因为Decimal甚至无法精确表示9.88131291682493E-324m(它将其表示为0),因为它的范围比Decimal小得多。但是,对于其支持范围内的数字,它要好得多。 - Matthew Watson
2
啊,概率计算。你绝对不想使用浮点数来进行计算。最好使用有理数。或者,由于永远不可能得到值为 0,可以将其视为始终是错误值。不存在概率为 0(也不存在概率为 1),这根本就没有意义。如果出现了 01,那么你已经失败了。 - Luaan
显示剩余3条评论
2个回答

6
Double 是按照浮点数规范IEEE 754精确工作的,所以这不是BCL中的错误 - 这只是IEEE 754浮点数的工作方式。
当然,原因是浮点数根本不是为此设计的。您可能希望使用decimal,它是一个精确的十进制数,不像float/double
浮点数有一些特殊值,具有不同的含义:
- 无穷大 - 例如1f / 0f。 - 负无穷大 - 例如-1f / 0f。 - NaN - 例如0f / 0fMath.Sqrt(-1) 但是,正如下面的评论者所指出的那样,虽然decimal确实检查溢出,但过于接近零并没有被视为溢出,就像浮点数一样。因此,如果您真的需要检查这一点,您将不得不自己制作*/方法。对于小数,您不应该真的在意。
如果您需要这种乘法和除法的精度(也就是说,您希望您的除法可以通过乘法反转),则应该使用有理数 - 两个整数(如果需要,可以使用大整数)。并使用checked上下文 - 这将在溢出时产生异常。
IEEE 754实际上确实处理下溢。有两个问题:
- 返回值为0(或负下溢为-1)。下溢的异常标志被设置,但在.NET中无法获取。 - 这仅发生在您接近零时失去精度。但是,在那之前,您已经失去了大部分精度。无论您拥有多么“精确”的数字,它都已经消失了-这些操作不可逆,也不是精确的。
因此,如果您真的关心可逆性等等,请坚持使用有理数。无论是C#还是其他语言,decimaldouble都不起作用。如果您不需要那么精确,那么您不应该在意下溢-只需选择最低合理数字,并将任何小于该数字的内容声明为“无效”即可;确保您远离实际的最大精度- double.Epsilon显然没有帮助。

在您看来,十进制数如何帮助解决这个问题? - Andrew Savinykh
@luaan 在这种情况下,问题不在于“decimal”与“double”的区别,而在于两者都具有有限的精度,都无法处理“9.88131291682493E-325”。 - xanatos
1
@MatthewWatson 我的理解是你的示例并不是浮点数误差问题。是错误的,因为在那种数据类型中无法准确地表示一个三分之一。 - Adam Houldsworth
顺便提一下,十进制类型是所有基本数字数据类型中性能最慢的。如果你的整数数字没有达到如此大的值,并且始终为正或零,请考虑使用ULong类型。 ULong数据类型(Visual Basic)的变量可以容纳从0到18,446,744,073,709,551,615(1.8...E + 19)的整数。ULong数字的操作比Decimal快得多,尽管不像UInteger那样高效。 - Paul Zahra
@MatthewWatson 的意思是,与 float 不同,它可以精确到给定的小数点。这模拟了人类大多数时候计算的方式 - 这是有用的属性。你仍然需要像人类一样Round结果(例如阅读一些计算税收的说明 - 总会有像“将两个数字相除,四舍五入到4位小数...”这样的内容;decimal允许您精确地执行此操作)。 - Luaan
显示剩余19条评论

5
你所需要的就是“epsilon”。
这是一个“小数”,它足够小,以至于你不再关心它。
你可以使用:
double epsilon = 1E-50;

“每当你的因子小于 epsilon 时,你会采取行动(例如将其视为0.0)。”

使用epsilon是一个好主意。对于概率计算来说,将任何结果为0的值转换为double.Epsilon是有意义的。 - SimpleVar
@YoryeNathan,是的,我喜欢那个。 - Andrew Savinykh
@zespri DrKoch的版本更聪明 - 如果你只是把每个零变成double.Epsilon,你可以轻松地从帽子里拿出这个数字。这比保持“几乎为零”的概率要糟糕得多。不仅失去了对称性,而且实际上能够产生任何可能的概率。我们有理由避免除以零:D - Luaan
我真的不知道你想用 epsilon 做什么,我主要是评论提到有一个已定义的 double.Epsilon。是你说的“如果由于任何原因你达到了 0 或 1,你就失败了。” - SimpleVar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接