haskell浮点计算异常?

Dvi*_*ius 5 floating-point precision haskell largenumber type-conversion

使用ghci 8.6.5

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

square :: Integer -> Integer
square m = floor $ sqrt $ fromInteger m
Run Code Online (Sandbox Code Playgroud)

有用。问题是,对于此特定的大数字作为输入:

4141414141414141 * 4141414141414141

我得到一个错误的结果。

放下我的功能,我在ghci中测试了这种情况:

> sqrt $ fromInteger $ 4141414141414141*4141414141414141
4.1414141414141405e15
Run Code Online (Sandbox Code Playgroud)

错...对吗?

简单地

> sqrt $ 4141414141414141*4141414141414141
4.141414141414141e15
Run Code Online (Sandbox Code Playgroud)

这更像我从计算中所期望的...

在我的函数中,我必须进行一些类型转换,我认为fromIntegral是必经之路。因此,使用该函数,我的函数对于4141 ... 41输入给出了错误的结果。

在运行sqrt之前,我无法弄清楚ghci在类型转换方面的隐含功能。因为ghci的转换允许正确的计算。

为什么我说这是异常现象:其他数字(如5151515151515151或3131313131313131或4242424242424242)不会发生此问题...

这是Haskell错误吗?

Dan*_*ner 6

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

一个明显的解决方案是返回Double没有小数部分的a,该a表示与Double存在的任何原始元素的绝对差最小的整数。不幸的是,这似乎不是GHC的决定fromInteger

相反,GHC的选择是返回Double最大幅度不超过原始数字幅度的。所以:

> 17151311090705026844052714160127 :: Double
1.7151311090705025e31
> 17151311090705026844052714160128 :: Double
1.7151311090705027e31
Run Code Online (Sandbox Code Playgroud)

(不要被第二个显示的数字有多短所迷惑:在Double它上面的行上有整数的确切表示;数字就停在那里,因为有足够的空间来唯一地标识一个Double。)

为什么这对您很重要?好吧,真正的答案4141414141414141*4141414141414141是:

> 4141414141414141*4141414141414141
17151311090705026668707274767881
Run Code Online (Sandbox Code Playgroud)

如果fromInteger将其转换为最接近的值Double(如上述计划(1)所示),它将选择1.7151311090705027e31。但是由于返回的值Double比上面的计划(2)中输入的值小,并且17151311090705026844052714160128从技术上讲更大,因此返回的精度不高1.7151311090705025e31

同时,4141414141414141它本身可以精确地表示为a Double,因此,如果您先转换为Double,然后转换为square,则会得到Double的语义,即选择最接近正确答案的表示形式,因此选择了方案(1)而不是方案(2)。

这解释了这种差异sqrt的输出:做你的计算中Integer最先得到一个确切的答案,然后转换为Double在最后一秒,矛盾的是不是转换成不太准确Double,因为如何立即用四舍五入的整条路上做你的计算,fromInteger不它的转换!哎哟。

我怀疑fromIntegerGHCHQ会优先考虑修改补丁以做得更好。无论如何,我知道会喜欢它的!


ali*_*ias 6

TLDR

归结为如何将一个Integer值转换Double为无法精确表示的值。请注意,这不能仅仅因为发生Integer过大(或过小),但FloatDouble通过设计值“快转”积分值作为其规模变大。因此,并非该范围内的每个整数值都可以精确表示。在这种情况下,实现必须基于舍入模式选择一个值。不幸的是,有多个候选人。而且您观察到的是Haskell挑选的候选人给您带来了更差的数值结果。

预期结果

包括Python在内的大多数语言都使用所谓的“从最接近的关系到偶数的舍入”舍入机制。这是默认的IEEE754舍入模式,除非您在兼容处理器中发布与浮点相关的指令时显式设置舍入模式,否则通常会得到此结果。在这里使用Python作为“参考”,我们得到:

>>> float(long(4141414141414141)*long(4141414141414141))
1.7151311090705027e+31
Run Code Online (Sandbox Code Playgroud)

我还没有尝试过使用其他支持大整数的语言,但是我希望大多数语言都能给您带来这种结果。

Haskell如何转换IntegerDouble

但是,Haskell使用了所谓的截断或舍入为零。这样就得到:

*Main> (fromIntegral $ 4141414141414141*4141414141414141) :: Double
1.7151311090705025e31
Run Code Online (Sandbox Code Playgroud)

事实证明,在这种情况下,这是一个“更差”的近似值(请参见上述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#
Run Code Online (Sandbox Code Playgroud)

依次调用:(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)
{
...
Run Code Online (Sandbox Code Playgroud)

其中故意说,转换完成后取整到零。

因此,这说明了您得到的行为。

为什么Haskell会这样做?

所有这些都不能解释为什么Haskell将“舍入到零”用于整数到双精度的转换。我强烈认为它应该使用默认的舍入模式,即,将最近的关系平整为偶数。我找不到任何提及这是否是一个有意识的选择,并且至少与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