Shogo's Blog

Oct 28, 2017 - 2 minute read - go golang

Go言語の浮動小数点数のお話

元ネタ:

コンピューターで浮動小数点数を扱ったことのある人なら一度は経験する、 数学上の計算とコンピューター上の計算が合わない計算の一例ですね。

この件に関して、Go言語では正しく(=数学的な結果と同じように)計算できるとの情報が。

しかしながら、inukiromさんのこの推察、半分はあってますが、半分は間違っていると思います。 なぜGo言語でこのような結果になったのか、検証してみました。

Goの数値定数の型について

以前Go言語でコンパイル時フィボナッチ数列計算で紹介した Better C - Go言語と整数 #golangにもあるように、 Goの定数には「型がない(場合がある)」「任意の精度で計算してくれる」という特徴があります。

このため、普通はどう考えてもオーバーフローしそうなこんな演算も・・・

package main

import (
	"fmt"
)

func main() {
	var i uint64 = 31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679 % 1000000007
	fmt.Println(i)
}

型がない定数同士の演算は 162132938 と正しい答えを出してくれます。

しかし、明示的に型を指定すると、今度はオーバーフローしてしまいます。

package main

import (
	"fmt"
)

func main() {
	var i uint64 = uint64(31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679) % 1000000007
	fmt.Println(i)
}
tmp/sandbox436519650/main.go:8:23: constant 31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679 overflows uint64

問題の計算

さて、最初の問題に戻りましょう。 以下のコードで、0.1, 0.2, 0.3 は「型のない定数」なので、「任意の精度で計算して」くれます。 その計算結果を float64 に変換すると全く同じ数値になるので、 ctrue になります。

package main

import (
	"fmt"
)

func main() {
	a := 0.3 - 0.2
	b := 0.2 - 0.1
	c := a == b
	fmt.Println(c)
}

一方、以下のように明示的に型を与えると、 float64 の精度でしか計算してくれません。 この場合は他のほとんどの言語同様、 cfalse となります。

package main

import (
	"fmt"
)

func main() {
	a := float64(0.3) - float64(0.2)
	b := float64(0.2) - float64(0.1)
	c := a == b
	fmt.Println(c)
}

おそらくGoはコンパイラがa=0.1とb=0.1に変換していると思われます。

というわけで、先程のinukiromさんのこの推察ツイートのこの部分は正解です。

計算されるのは実行時?コンパイル時?

次にこんな二つのコードを用意して GOSSAFUNC=main go run main.go を実行し、 SSAによる最適化の様子を見てみます。 違いは x, y, zconst で宣言されているか、var で宣言されているか、だけです。

package main

import (
	"fmt"
)

func main() {
	var (
		x = 0.3
		y = 0.2
		z = 0.1
	)
	a := x - y
	b := y - z
	c := a == b
	fmt.Println(c)
}
package main

import (
	"fmt"
)

func main() {
	const (
		x = 0.3
		y = 0.2
		z = 0.1
	)
	a := x - y
	b := y - z
	c := a == b
	fmt.Println(c)
}

結果は以下のツイートの通り。

最適化の結果コンパイル時に計算が行われ、(計算結果に多少の誤差はありますが) var の場合でも const の場合でも x, y, z は消えてしまいました。

constはコンパイル時に計算されますが、varは実行時に計算されるためです。

そういうわけで、この部分は間違いです。 Goのコンパイラは賢いので、 var であってもコンパイル時に計算可能ならば計算してくれます。 (比較演算子は範囲外?みたいだけど・・・)

任意の精度で計算の限界に迫る

ここまでは abfloat64 という型を持っていました。 次に以下のように書き換えて ab も「型の無い定数」にしてみましょう。 すると少し面白い結果が得られます。

package main

import (
	"fmt"
)

func main() {
	const a = 0.3 - 0.2
	const b = 0.2 - 0.1
	var c = a == b
	fmt.Println(c)
	fmt.Printf("%e\n", float64(a-b))
}
false
9.322926e-156

cfalse になってしまいました。 「任意の精度で計算」と言ってもコンピューター上の計算である以上、有効桁数には限界があります。 ab の差が 9.322926e-156 になったことから、おそらく有効桁数150桁程度で計算していると考えられます。

ここでちょっとソースコードを覗いてみると・・・

512bitの精度で計算しているようです。 $$512 \times \log 2 = 154.1273577…$$ なので、有効桁数150桁程度という予想通りです。

まとめ

何事にも限界ってものがある。