この記事はkb Advent Calendar 2020 10日目の記事です。 https://adventar.org/calendars/5280
Goのfloatの扱い
詳しい挙動については以下が詳しい
https://qiita.com/sonatard/items/eac6fb35dcc8e052a293
goの浮動小数点の扱いとコンパイラの最適化を確かめるため、以下の様なコードを実行してみる。
package main
import (
"fmt"
)
func test0() bool {
return true
}
func test1() bool {
r := 0.1+0.2 == 0.3
return r
}
func test2() bool {
a := float64(0.1)
b := float64(0.2)
r := a + b == 0.3
return r
}
func test3() bool {
a := 0.1
b := 0.2
r := a + b == 0.3
return r
}
func test4() bool {
a := float32(0.1)
b := float32(0.2)
r := a + b == 0.3
return r
}
func test5() bool {
a := float32(0.3)
b := float32(0.2)
r := a - b == 0.1
return r
}
func main() {
fmt.Println(test0())
fmt.Println(test1())
fmt.Println(test2())
fmt.Println(test3())
fmt.Println(test4())
fmt.Println(test5())
}
上記のコードの出力結果は何になるだろうか。実行してみると以下のようになる(go1.15)。
true
true
false
false
true
false
何故そうなるのか確認してみる。
delveなどのdisassemblerを用いても良いが、SSAの出力を見るのが一番簡単だと思われる。
以下でSSAのdumpを出力する
$ go tool compile -d 'ssa/build/dump=test0' test.go
$ go tool compile -d 'ssa/build/dump=test1' test.go
$ go tool compile -d 'ssa/build/dump=test2' test.go
$ go tool compile -d 'ssa/build/dump=test3' test.go
$ go tool compile -d 'ssa/build/dump=test4' test.go
$ go tool compile -d 'ssa/build/dump=test5' test.go
test0のSSAの出力は以下のようになる。
test0 func() bool
b1: // ブロックのラベル
v1 = InitMem <mem> // ヒープの初期化
v2 = SP <uintptr> // スタックポインタ
v3 = SB <uintptr> DEAD // スタックベース
v4 = LocalAddr <*bool> {~r0} v2 v1 // boolのローカルアドレス
v5 = ConstBool <bool> [false] DEAD // bool値の定数 宣言部分(デッドコード)
v6 = ConstBool <bool> [true] // bool値の定数 trueが返る
v7 = VarDef <mem> {~r0} v1 // v1参照用の変数r0を定義
v8 = Store <mem> {bool} v4 v6 v7 // v7のメモリにv4/v6/v7を格納
Ret v8 // return
これを基準にしてコードの最適化を見てみる。
test1の出力は以下である。
test1 func() bool
b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v3 = SB <uintptr> DEAD
v4 = LocalAddr <*bool> {~r0} v2 v1
v5 = ConstBool <bool> [false] DEAD
v6 = ConstBool <bool> [true] (r[bool])
v7 = VarDef <mem> {~r0} v1
v8 = Store <mem> {bool} v4 v6 v7
Ret v8
name r[bool]: [v6]
変わった部分は (r[bool])
が付いたのと、変数のラベルが新たに追加されたくらいである。
つまり、test1の 0.1+0.2 == 0.3
の計算はコンパイル時に行っており、結局trueを返すことしかしていない。
float64
次にtest2を見てみる。
float64でそのまま値を扱っていることがわかる。
test2 func() bool
b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v3 = SB <uintptr> DEAD
v4 = LocalAddr <*bool> {~r0} v2 v1
v5 = ConstBool <bool> [false] DEAD
v6 = Const64F <float64> [0.1] (a[float64])
v7 = Const64F <float64> [0.2] (b[float64])
v8 = Add64F <float64> v6 v7
v9 = Const64F <float64> [0.3]
v10 = Eq64F <bool> v8 v9 (r[bool])
v11 = VarDef <mem> {~r0} v1
v12 = Store <mem> {bool} v4 v10 v11
Ret v12
name a[float64]: [v6]
name b[float64]: [v7]
name r[bool]: [v10]
float64の精度で計算後、Equalを行っており、丸め誤差のある計算結果と0.3の差分が原因でfalseになったと思われる。
fmt.Printf( "%.64f\n",float64(0.1)+float64(0.2))
fmt.Printf( "%.64f\n",float64(0.3))
// 0.3000000000000000444089209850062616169452667236328125000000000000
// 0.2999999999999999888977697537484345957636833190917968750000000000
ちなみに test3 の出力は一緒になるので、結果は同じくfalseとなる。
$ diff test2_01__build.dump test3_01__build.dump
1c1
< test2 func() bool
---
> test3 func() bool
float32
次に test4 を見てみる
test4 func() bool
b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v3 = SB <uintptr> DEAD
v4 = LocalAddr <*bool> {~r0} v2 v1
v5 = ConstBool <bool> [false] DEAD
v6 = Const32F <float32> [0.10000000149011612] (a[float32])
v7 = Const32F <float32> [0.20000000298023224] (b[float32])
v8 = Add32F <float32> v6 v7
v9 = Const32F <float32> [0.30000001192092896]
v10 = Eq32F <bool> v8 v9 (r[bool])
v11 = VarDef <mem> {~r0} v1
v12 = Store <mem> {bool} v4 v10 v11
Ret v12
name a[float32]: [v6]
name b[float32]: [v7]
name r[bool]: [v10]
コンパイラ上で既にFloatの0.1/0.2及び0.3を任意の精度の定数で扱っている。
test5の方も見てみる。
test5 func() bool
b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v3 = SB <uintptr> DEAD
v4 = LocalAddr <*bool> {~r0} v2 v1
v5 = ConstBool <bool> [false] DEAD
v6 = Const32F <float32> [0.30000001192092896] (a[float32])
v7 = Const32F <float32> [0.20000000298023224] (b[float32])
v8 = Sub32F <float32> v6 v7
v9 = Const32F <float32> [0.10000000149011612]
v10 = Eq32F <bool> v8 v9 (r[bool])
v11 = VarDef <mem> {~r0} v1
v12 = Store <mem> {bool} v4 v10 v11
Ret v12
name a[float32]: [v6]
name b[float32]: [v7]
name r[bool]: [v10]
定数の数値は変わらない。
では、何故 0.1 + 0.2 == 0.3
はtrueで、0.3 - 0.2 == 0.1
はfalseとなるのか。
以下を実行してみる。
fmt.Printf("float32(0.1) = %.32f\n", float32(0.1))
fmt.Printf("float32(0.3) = %.32f\n", float32(0.3))
fmt.Printf("float32(0.1+0.2) = %.32f\n", float32(0.1+0.2))
fmt.Printf("float32(0.1) + float32(0.2) = %.32f\n", float32(0.1) + float32(0.2))
fmt.Printf("float32(0.3-0.2) = %.32f\n", float32(0.3-0.2))
fmt.Printf("float32(0.3) - float32(0.2) = %.32f\n", float32(0.3) - float32(0.2))
fmt.Println( float32(0.1)+float32(0.2)==float32(0.3))
fmt.Println( float32(0.3)-float32(0.2)==float32(0.1))
出力は以下のようになり、 0.3
と 0.1 + 0.2
の結果は同じ値だが、0.1
と 0.3 - 0.2
は同じ値になっていない。おそらく除算時は桁落ち誤差が起こっているのだと思われる。
float32(0.1) = 0.10000000149011611938476562500000
float32(0.3) = 0.30000001192092895507812500000000
float32(0.1+0.2) = 0.30000001192092895507812500000000
float32(0.1) + float32(0.2) = 0.30000001192092895507812500000000
float32(0.3-0.2) = 0.10000000149011611938476562500000
float32(0.3) - float32(0.2) = 0.10000000894069671630859375000000
true
false
じゃあ除算のときは絶対に一致しないかというと、実はEq32F
自体の精度次第なだけではあるっぽく、
fmt.Println( 0.100000006==float32(0.1)) // false
fmt.Println( 0.100000005==float32(0.1)) // true
fmt.Println( 0.099999998==float32(0.1)) // true
fmt.Println( 0.099999997==float32(0.1)) // false
上記のコードでは両方trueが返るため、0.099999998 < x < 0.100000005
の範囲に収まっていれば一致と判定されるみたいである。
というわけで、goで浮動小数点を扱う際は、math/big
や任意の精度で扱おう。
参考: