1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Goのfloat32/float64

Last updated at Posted at 2020-12-09

この記事は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 や任意の精度で扱おう。

参考:

1
2
0

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
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?