Goのfloat32/float64

この記事はkb Advent Calendar 2020 10日目の記事です。 https://adventar.org/calendars/5280

## Goのfloatの扱い

詳しい挙動については以下が詳しい
https://qiita.com/sonatard/items/eac6fb35dcc8e052a293

goの浮動小数点の扱いとコンパイラの最適化を確かめるため、以下の様なコードを実行してみる。

```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](https://pkg.go.dev/golang.org/x/tools/go/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`](https://golang.org/pkg/math/big/) や任意の精度で扱おう。


参考:

- https://ren.nosuke.me/blog/202002/20200227/
- https://stackoverflow.com/questions/46374304/dealing-with-floating-point-number-precision-in-go-arithmetic
- https://blog.amedama.jp/entry/2016/04/07/215131
- https://qiita.com/sonatard/items/eac6fb35dcc8e052a293