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
太大(或太小),而且 Float
和 Double
值在设计上“跳过”整数值,因为它们的幅度变大。因此,并非范围内的每个整数值都可以精确表示。在这种情况下,实现必须根据舍入模式选择一个值。不幸的是,有多个候选人;而您观察到的是 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
的调用确实是转移注意力。
显示代码
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
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
太大(或太小),而且 Float
和 Double
值在设计上“跳过”整数值,因为它们的幅度变大。因此,并非范围内的每个整数值都可以精确表示。在这种情况下,实现必须根据舍入模式选择一个值。不幸的是,有多个候选人;而您观察到的是 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
的调用确实是转移注意力。
显示代码
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