浮点算法 – 双类型的模算子

所以我试图找出为什么模运算符返回如此大的exception值。

如果我有代码:

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上阅读了这个主题,在下面的post中有 两 三个 ,其中包括。

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

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

出现错误是因为double不能精确地表示0.1 – 它可以表示的最接近的值是0.100000000000000005551115123126。 现在,当你将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.9999999999999944488848768742172978818416595458984375(舍入到80位) )。 现在,如果你将它舍入到64位,它将是10.0,但如果你保持80位内部并在其上发言,它将截断为9.0,因此得到.0999999999999999500399638918679556809365749359130859375作为最终答案。

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

0.1不能完全用二进制表示的事实与它有关。

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

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

还要注意/是一个主要是连续的函数(参数上的一个小差异通常意味着结果上的一个小差异,虽然导数可能很大但接近于零的同一侧,至少额外的参数精度有帮助) 。 另一方面,%不是连续的:无论你选择何种精度,总会有一些参数,第一个参数上任意小的表示错误意味着结果出现大的错误。

指定IEEE 754的方式,假设参数完全符合您的要求,您只能得到一个浮点运算结果近似的保证。 如果参数不是您想要的,您需要切换到其他解决方案,例如区间运算或分析程序的良好条件(如果它在浮点数上使用%,则可能不会很好-conditioned)。

这不是计算中的“错误”,而是你从未真正开始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时,您的结果(在显示舍入后)为0.09999999999999995 == (0.1 - 5e-17) 。 但请记住,这几乎是0.1,而0.1 % 0.1 == 0

所以你的实际误差是-5e-17 。 我会称之为小。

根据您需要的数字,最好写一下:

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