C# 双精度浮点算术-模运算符

C# 双精度浮点算术-模运算符,c#,floating-point,precision,C#,Floating Point,Precision,所以我想弄明白为什么模运算符返回这么大的异常值 如果我有密码: double result=1.0d%0.1d 它将给出0.09999999995的结果。我希望值0 注意:使用除法运算符不存在此问题- double result=1.0d/0.1d 将给出10.0的结果,这意味着余数应为0 让我明确一点:我对错误的存在并不感到惊讶,我感到惊讶的是,与当前的数字相比,错误如此之大。0.0999~=0.1和0.1与0.1d处于同一个数量级,与1.0d仅相差一个数量级。它不像你可以将它与double.

所以我想弄明白为什么模运算符返回这么大的异常值

如果我有密码:

double result=1.0d%0.1d

它将给出
0.09999999995
的结果。我希望值
0

注意:使用除法运算符不存在此问题-
double result=1.0d/0.1d

将给出
10.0
的结果,这意味着余数应为
0

让我明确一点:我对错误的存在并不感到惊讶,我感到惊讶的是,与当前的数字相比,错误如此之大。0.0999~=0.1和0.1与
0.1d
处于同一个数量级,与
1.0d
仅相差一个数量级。它不像你可以将它与double.epsilon进行比较,或者说“如果它的差值小于0.00001,它就等于”

我在下面的帖子中读到了关于StackOverflow的这个主题

有人能解释为什么这个错误如此之大吗?任何避免在将来遇到问题的建议(我知道我可以使用decimal,但我担心它的性能)

编辑:我应该特别指出,我知道0.1是一个-这与它有什么关系吗?

这并不完全是计算中的一个“错误”,而是一个事实,即你从未真正从0.1开始

问题是1.0可以用二进制浮点精确地表示,但0.1不能,因为它不能用2的负幂精确地构造。(是1/16+1/32+…)

所以你没有得到1.0%0.1,机器只能计算1.0%0.1+-0.00。。。然后它诚实地报告结果


为了得到一个大的余数,我假设
%
的第二个操作数必须略大于0.1,从而阻止了最后的除法,并导致几乎整个0.1都是运算的结果。

0.1不能用二进制精确表示的事实与此有关

如果0.1可以表示为
double
,那么您将获得与要计算的操作的实际结果最接近的可表示双精度(假设为“最接近的”舍入模式)

因为它不能,所以您得到的是最接近一个操作的可表示双精度,这个操作与您试图计算的操作完全不同

还要注意的是/基本上是一个连续函数(参数上的一个小差异通常意味着结果上的一个小差异,虽然导数可以大到接近零但在零的同一侧,但至少参数的额外精度会有所帮助)。另一方面,%不是连续的:无论您选择的精度如何,总会有一些参数,其中第一个参数上任意小的表示错误意味着结果上的大错误


按照IEEE 754的指定方式,假设参数正是您想要的,您只能得到一个浮点运算结果的近似值的保证。如果参数不完全是您想要的,您需要切换到其他解决方案,例如区间算术或程序的良好条件分析(如果它在浮点数上使用%,则可能条件不好)该错误的产生是因为双精度不能精确表示0.1——它所能表示的最接近值大约为0.1000000000000005555115123126。现在,当你除以1.0,它会给你一个略小于10的数字,但是一个double不能准确地表示它,所以它最后四舍五入到10。但是当你做mod时,它可以给你略小于0.1的余数

由于0=0.1 mod 0.1,mod中的实际误差为0.1-0.09999999…--非常小

如果将%运算符的结果添加到9*0.1,它将再次给出1.0

编辑

关于舍入的细节再详细一点——特别是这个问题是混合精度危险的一个很好的例子

浮点数的
a%b
计算方法通常为
a-(b*floor(a/b))
。问题是,它可能一次完成,内部精度比那些操作要高(并在每个阶段将结果四舍五入到一个fp数),因此可能会得到不同的结果。很多人看到的一个例子是,英特尔x86/x87硬件使用80位精度进行中间计算,而内存中的值仅使用64位精度。因此,上面等式中
b
中的值来自内存,因此是一个不完全为0.1的64位fp数(感谢dan04提供了精确值),因此当它计算1.0/0.1时,得到9.99999999999999994448884876874217288818416595458984375(四舍五入到80位)。现在,如果你把它四舍五入到64位,它将是10.0,但是如果你保持80位的内部,在它上面做底图,它将截断为9.0,因此得到.0999999999950039963891867955680965749359130859375作为最终答案


在这种情况下,你会看到一个很大的明显错误,因为你使用了一个不连续的步长函数(floor),这意味着内部值的一个非常微小的差异可以推动你越过步长。但是由于mod本身是一个非连续的阶跃函数,这是可以预期的,这里的实际误差是0.1-0.0999。。。因为0.1是mod函数范围内的不连续点。

您看到的误差很小;乍一看它看起来很大。 您的结果(四舍五入显示后)为
0.09999999995==(0.1-5e-17)
当您希望从
%0.1
操作。 但请记住,这几乎是0.1,并且
0.1%0.1==0

所以这里的实际错误是
-5e-17
。我称之为sm