关于c#:浮点算术 – 双类型上的模运算符

Floating Point Arithmetic - Modulo Operator on Double Type

所以我想知道为什么模运算符返回如此大的异常值。

如果我有密码:

double result = 1.0d % 0.1d;

它将给出0.09999999999999995的结果。我期望值为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的这个主题,其中包括一二三。

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

编辑:我应该特别指出,我知道0.1是一个无限重复的二进制数字序列-这与它有什么关系吗?


出现错误的原因是double不能精确地表示0.1——它能表示的最接近的值是0.10000000005551115123126。现在,当你将1.0除以它时,它会给你一个略小于10的数字,但同样,一个双精度数不能精确地表示它,所以它最后会四舍五入到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中的值来自内存,因此是一个64位的fp数,不完全是0.1(感谢dan04提供精确的值),所以当它计算1.0/0.1时,它得到9.999999999999944888768742172978818416595458984375(四舍五入到80位)。现在,如果把它四舍五入到64位,它将是10.0,但是如果将80位保持在内部并在其上做底板,它将截为9.0,因此得到.099999999999500399638918679556809365749359130859375作为最终答案。

因此,在本例中,您会看到一个明显的错误,因为您使用的是一个不连续的步骤函数(floor),这意味着内部值的微小差异可以将您推过步骤。但由于mod本身是一个不连续的步进函数,所以这里的实际错误是0.1-0.0999…因为0.1是mod函数范围内的不连续点。


0.1不能精确地用二进制表示这一事实与此有关。

如果0.1可以表示为double,则您将得到可表示的双最近(假设为"最近"舍入模式)与要计算的操作的实际结果。

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

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

按照指定IEEE754的方式,您只获得一个浮点运算结果的近似值的保证,前提是参数正是您想要的。如果参数不完全符合您的要求,则需要切换到其他解决方案,例如间隔算术或分析程序的良好条件(如果它对浮点数使用了%的值,则很可能没有很好的条件)。


这并不是计算中的一个"错误",而是你从来没有真正的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,而0.1 % 0.1 == 0

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

根据您需要的号码,最好写下:

double result = 1.0 % 0.1;
result = result >= 0.1/2 ? result - 0.1 : result;