LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

Goのfloat32/float64

この記事は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.30.1 + 0.2 の結果は同じ値だが、0.10.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 や任意の精度で扱おう。

参考:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2