haskell浮点运算异常?

A haskell floating point calculation anomaly?

2022 更新:此错误已作为 GHC 票证提交,现已修复:https://gitlab.haskell.org/ghc/ghc/issues/17231 所以这不再是问题。

使用 ghci 8.6.5

我想计算整数输入的平方根,然后将其四舍五入到底部,return 一个整数。

square :: Integer -> Integer
square m = floor $ sqrt $ fromInteger m

有效。 问题是,对于这个特定的大数字作为输入:

4141414141414141*4141414141414141

我得到了错误的结果。

撇开我的功能,我在ghci中测试案例:

> sqrt $ fromInteger $ 4141414141414141*4141414141414141
4.1414141414141405e15

错...对吗?

但很简单

> sqrt $ 4141414141414141*4141414141414141
4.141414141414141e15

这更符合我对计算的预期...

在我的函数中,我必须进行一些类型转换,我认为 fromIntegral 是可行的方法。因此,使用它,我的函数为 4141...41 输入给出了错误的结果。

我无法弄清楚 ghci 在 运行 sqrt 之前在类型转换方面隐式做了什么。因为ghci的转换允许正确计算。

为什么我说这是一个异常现象:问题不会发生在其他号码上,例如 5151515151515151 或 3131313131313131 或 4242424242424242 ...

这是 Haskell 错误吗?

并非所有 Integer 都可以完全表示为 Double。对于那些不是的人,fromInteger 处于需要做出选择的不利位置:return 应该选择哪个 Double?我在报告中找不到任何讨论在这里做什么的内容,哇!

一个明显的解决方案是 return 一个没有小数部分的 Double,它表示与现有任何 Double 中的原始整数绝对差值最小的整数。不幸的是,这似乎不是 GHC 的 fromInteger.

做出的决定

相反,GHC的选择是return最大量级不超过原数的Double。所以:

> 17151311090705026844052714160127 :: Double
1.7151311090705025e31
> 17151311090705026844052714160128 :: Double
1.7151311090705027e31

(不要被第二个显示的数字有多短所迷惑:Double 在它上面的行中有整数的精确表示;数字停在那里是因为有足以唯一标识单个 Double.)

为什么这对您很重要?那么,4141414141414141*4141414141414141 的正确答案是:

> 4141414141414141*4141414141414141
17151311090705026668707274767881

如果fromInteger将其转换为最接近的Double,如上述方案(1),它会选择1.7151311090705027e31。但由于它 return 最大 Double 小于上面计划 (2) 中的输入,并且 17151311090705026844052714160128 在技术上更大,因此 return 表示不太准确 1.7151311090705025e31.

同时,4141414141414141 本身可以精确表示为 Double,因此如果您首先转换为 Double,然后平方,您将获得 Double 的语义选择最接近正确答案的表示,因此计划 (1) 而不是计划 (2)。

这解释了 sqrt 输出中的差异:首先在 Integer 中进行计算并获得准确答案,然后在最后一秒转换为 Double,矛盾的是不如立即转换为 Double 并通过四舍五入进行计算,因为 fromInteger 如何进行转换!哎哟

我怀疑 GHCHQ 会看好修改 fromInteger 以做一些更好的事情的补丁;无论如何我知道会喜欢它!

TLDR

它归结为如何将 Integer 值转换为无法完全表示的 Double。请注意,发生这种情况不仅是因为 Integer 太大(或太小),而且 FloatDouble 值在设计上“跳过”整数值,因为它们的幅度变大。因此,并非范围内的每个整数值都可以精确表示。在这种情况下,实现必须根据舍入模式选择一个值。不幸的是,有多个候选人;而您观察到的是 Haskell 选择的候选人给您的数值结果更差。

预期结果

大多数语言,包括 Python,都使用所谓的“round-to-nearest-ties-to-even”舍入机制;这是默认的 IEEE754 舍入模式,除非您在兼容处理器中发出浮点相关指令时明确设置舍入模式,否则通常会得到这种模式。在这里使用 Python 作为“参考”,我们得到:

>>> float(long(4141414141414141)*long(4141414141414141))
1.7151311090705027e+31

我还没有尝试过其他支持所谓的大整数的语言,但我希望它们中的大多数都能给你这个结果。

Haskell 如何将 Integer 转换为 Double

然而,

Haskell 使用所谓的 截断 或向零舍入。所以你得到:

*Main> (fromIntegral $ 4141414141414141*4141414141414141) :: Double
1.7151311090705025e31

事实证明,在这种情况下这是一个“更差”的近似值(参见上面的 Python 产生的值),并且您在原始示例中得到了意想不到的结果。

此时对 sqrt 的调用确实是转移注意力。

显示代码

一切源于这段代码:(https://hackage.haskell.org/package/integer-gmp-1.0.2.0/docs/src/GHC.Integer.Type.html#doubleFromInteger)

doubleFromInteger :: Integer -> Double#
doubleFromInteger (S# m#) = int2Double# m#
doubleFromInteger (Jp# bn@(BN# bn#))
    = c_mpn_get_d bn# (sizeofBigNat# bn) 0#
doubleFromInteger (Jn# bn@(BN# bn#))
    = c_mpn_get_d bn# (negateInt# (sizeofBigNat# bn)) 0#

依次调用:(https://github.com/ghc/ghc/blob/master/libraries/integer-gmp/cbits/wrappers.c#L183-L190):

/* Convert bignum to a `double`, truncating if necessary
 * (i.e. rounding towards zero).
 *
 * sign of mp_size_t argument controls sign of converted double
 */
HsDouble
integer_gmp_mpn_get_d (const mp_limb_t sp[], const mp_size_t sn,
                       const HsInt exponent)
{
...

故意表示转换已完成向零舍入。

所以,这解释了你得到的行为。

为什么 Haskell 这样做?

None 这解释了为什么 Haskell 使用舍入到零来进行整数到双精度的转换。我强烈认为它应该使用默认的舍入模式,即 round-nearest-ties-to-even。我找不到任何提及这是否是一个有意识的选择,它至少不同意 Python 所做的。 (并不是说我会认为 Python 是黄金标准,但它确实会把这些事情做好。)

我最好的猜测是它只是这样编码的,没有有意识的选择;但也许其他熟悉 Haskell 中数值编程历史的人会记得更好。

做什么

有趣的是,我发现以下讨论可以追溯到 2008 年,作为 Python 错误:https://bugs.python.org/issue3166。显然,Python 过去也曾在这里做错事,但他们修复了这种行为。很难追踪确切的历史,但 Haskell 和 Python 似乎都犯了同样的错误; Python 恢复了,但在 Haskell 中没有引起注意。如果这是一个有意识的选择,我想知道为什么。

所以,这就是它的立场。我建议打开一张 GHC 票,这样至少可以正确记录这是“选择的”行为;或者更好,修复它,使其使用默认的舍入模式。

更新:

已打开 GHC 票证:https://gitlab.haskell.org/ghc/ghc/issues/17231

2022 年更新:

这在 GHC 中已经修复;至少从 GHC 9.2.2 开始;但可能更早:

GHCi, version 9.2.2: https://www.haskell.org/ghc/  :? for help
Prelude> (fromIntegral $ 4141414141414141*4141414141414141) :: Double
1.7151311090705027e31