Haskellでの(^)の奇妙な動作


12

GHCiが以下の不正解を返すのはなぜですか?

GHCi

λ> ((-20.24373193905347)^12)^2 - ((-20.24373193905347)^24)
4.503599627370496e15

Python3

>>> ((-20.24373193905347)**12)**2 - ((-20.24373193905347)**24)
0.0

更新 Haskellの(^)関数を次のように実装します。

powerXY :: Double -> Int -> Double
powerXY x 0 = 1
powerXY x y
    | y < 0 = powerXY (1/x) (-y)
    | otherwise = 
        let z = powerXY x (y `div` 2)
        in  if odd y then z*z*x else z*z

main = do 
    let x = -20.24373193905347
    print $ powerXY (powerXY x 12) 2 - powerXY x 24 -- 0
    print $ ((x^12)^2) - (x ^ 24) -- 4.503599627370496e15

私のバージョンは@WillemVanOnsemによって以下に提供されているバージョンよりも正確に見えませんが、少なくともこの特定のケースでは奇妙な正解を示しています。

Pythonも同様です。

def pw(x, y):
    if y < 0:
        return pw(1/x, -y)
    if y == 0:
        return 1
    z = pw(x, y//2)
    if y % 2 == 1:
        return z*z*x
    else:
        return z*z

# prints 0.0
print(pw(pw(-20.24373193905347, 12), 2) - pw(-20.24373193905347, 24))

これは仮数に関するエラーです。a^24は約2.2437e31であるため、これを生成する丸め誤差があります。
Willem Van Onsem

わかりません。GHCiに丸め誤差があるのはなぜですか?
ランダムな男

これはghciとは関係なく、単に浮動小数点ユニットが浮動小数点を処理する方法です。
Willem Van Onsem

1
2.243746917640863e31 - 2.2437469176408626e31これは、増幅される小さな丸め誤差を計算します。キャンセルの問題のようです。
カイ

2
たぶんpythonはべき乗に異なるアルゴリズムを使用しているかもしれませんが、この場合はより正確ですか?一般に、使用する言語に関係なく、浮動小数点演算では丸め誤差が発生します。それでも、2つのアルゴリズムの違いを理解するのは興味深いかもしれません。
カイ

回答:


14

短い答え:とには違いが(^) :: (Num a, Integral b) => a -> b -> aあり (**) :: Floating a => a -> a -> aます。

この(^)関数は、積分指数でのみ機能します。これは通常、累乗が2で割り切れるかどうかを毎回チェックし、累乗を2で除算する(そして割り切れない場合は結果をで乗算するx)反復アルゴリズムを使用します。したがって12、これは、の場合、合計6つの乗算を実行することを意味します。乗算に丸め誤差がある場合、その誤差は「爆発」する可能性があります。ソースコードを見るとわかるように、(^)関数は次のように実装されています。

(^) :: (Num a, Integral b) => a -> b -> a
x0 ^ y0 | y0 < 0    = errorWithoutStackTrace "Negative exponent"
        | y0 == 0   = 1
        | otherwise = f x0 y0
    where -- f : x0 ^ y0 = x ^ y
          f x y | even y    = f (x * x) (y `quot` 2)
                | y == 1    = x
                | otherwise = g (x * x) (y `quot` 2) x         -- See Note [Half of y - 1]
          -- g : x0 ^ y0 = (x ^ y) * z
          g x y z | even y = g (x * x) (y `quot` 2) z
                  | y == 1 = x * z
                  | otherwise = g (x * x) (y `quot` 2) (x * z) -- See Note [Half of y - 1]

(**)関数は、少なくともため、あるFloatSおよびDouble浮動小数点ユニット上で動作するように実装sです。実際、の実装(**)を見ると、次のことがわかります。

instance Floating Float where
    -- …
    (**) x y = powerFloat x y
    -- …

これにより、powerFloat# :: Float# -> Float# -> Float#関数にリダイレクトされます。関数は通常、コンパイラーによって対応するFPU操作にリンクされます。

(**)代わりに使用すると、64ビットの浮動小数点ユニットに対してもゼロが取得されます。

Prelude> (a**12)**2 - a**24
0.0

たとえば、Pythonで反復アルゴリズムを実装できます。

def pw(x0, y0):
    if y0 < 0:
        raise Error()
    if y0 == 0:
        return 1
    return f(x0, y0)


def f(x, y):
    if (y % 2 == 0):
        return f(x*x, y//2)
    if y == 1:
        return x
    return g(x*x, y // 2, x)


def g(x, y, z):
    if (y % 2 == 0):
        return g(x*x, y//2, z)
    if y == 1:
        return x*z
    return g(x*x, y//2, x*z)

その後、同じ操作を実行すると、ローカルで取得できます。

>>> pw(pw(-20.24373193905347, 12), 2) - pw(-20.24373193905347, 24)
4503599627370496.0

これは、(^)GHCiで得られるものと同じ値です。


1
Pythonで実装された(^)の反復アルゴリズムでは、この丸め誤差は発生しません。(*)HaskellとPythonでは違いがありますか?
ランダムな男

@Randomdude:私の知る限りpow(..)、Python の関数には、浮動小数点数ではなく、「int / long」の特定のアルゴリズムしかありません。フロートの場合、FPUの能力で「フォールバック」します。
Willem Van Onsem

つまり、Haskellによる(^)の実装と同じ方法で、Pythonで(*)を使用して自分でパワー関数を実装するということです。pow()関数を使用していません。
ランダムな男

2
@Randomdude:Pythonでアルゴリズムを実装し、ghcと同じ値を取得しました。
Willem Van Onsem

1
HaskellとPythonの私のバージョンの(^)で質問を更新しました。考えてください?
ランダムな男
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.