2
3

浮動小数点

Goには2つの浮動小数点型がある。

  • float32
  • float64

Go Playground

var n float32 = 1.0001
fmt.Println(n * n) // 1.0002

1.0001 * 1.0001 = 1.00020001ですが、1.0002と出力されます。

小数点は無限に存在するが、float64は有限の64bitである。そのため、丸め誤差が生じ、精度が落ちる時がある。

単精度浮動小数点(float32) : 指数が8ビット、23ビットが仮数を表す。
倍精度浮動小数点型(float64) : 指数が11ビット、仮数が52ビット、残りは符号となる。

符号 $S$ 、仮数 $F$ 、指数 $E$ とすると、10進数 $a$ は以下のようになる。

$ a = (-1)^ S \times F \times 2^E $

1.0001を上記の式で表すと、

1.0001 = 1  \times 1.000100016593933 \times 2^0

となるので、1.0001をfloat32で表すと、以下のようになる。

符号 指数(8bit) 仮数(23bit)
0 01111111 00000000000001101000111

近似値を使うことによる弊害

比較

近似値を使うことで、==を使用した比較が不正確になる場合がある。

goのテストライブラリtestify には比較している値が$\delta$の範囲内であることを検査するためのInDeltaが存在する。

プロセッサに依存する

プロセッサには、浮動小数点演算を処理する浮動小数点演算ユニット(Floating Point Unit, FPU)を持っている。
処理結果はFPUに依存するので、FPUが異なる別のマシンで同じ結果になる保証はない。

こちらも$\delta$を考えることで解決する。

特殊な浮動小数点数

  • 正の無限
  • 負の無限
  • NaN(Not-aNumber)

NaNはf != fを満たす唯一の浮動小数点数である。

これらの3つの浮動小数点数は以下のように作成される。

Go Playground

func main() {
	var a float64
	positiveInf := 1 / a
	negativeInf := -1 / a
	nan := a / a
	fmt.Println(positiveInf, negativeInf, nan) // +Inf -Inf NaN
}

これら3つの浮動小数点数についての比較は、math.IsInf(無限)で、math.IsNan(NaN)で比較できる。

誤差の蓄積

加算・減算

以下のコードは10000に1.0001をn回加算プログラム。

func f1(n int) float64 {
	result := 10_000.
	for i := 0; i < n; i++ {
		result += 1.0001
	}
	return result
}

それに対して、f2は1.0001をn回加算して、最後に10000を加算するプログラム。

func f2(n int) float64 {
	result := 0.
	for i := 0; i < n; i++ {
		result += 1.0001
	}
	return result + 10_000.
}

nの値を変えて実行すると、

func main() {
	n1 := 10
	fmt.Println(f1(n1), f2(n1)) // 10010.000999999993 10010.001
	n2 := 1000
	fmt.Println(f1(n2), f2(n2)) // 11000.099999999293 11000.099999999982
	n3 := 1000000
	fmt.Println(f1(n3), f2(n3)) // 1.0100999999761417e+06 1.0100999999766762e+06
}

nが大きくなるにつれて誤差が大きくなる。しかし、f2の方が精度は高い。

n 正確な値 f1 f2
10 10010.001 10010.000999999993 10010.001
1k 11000.1 11000.099999999293 11000.099999999982
1m 1.0101e+06 1.0100999999761417e+06 1.0100999999766762e+06

加算や減算を連続して行う場合は、大きさの近い値をまとめて計算してから、大きな値を加算・減算することで、誤差が小さくなる。

乗算・除算

$a \times (b+c)$ と $ a \times b + a \times c$ の結果は異なる。

func main() {
	a := 100000.001
	b := 1.0001
	c := 1.0002

	fmt.Println(a * (b + c)) // 200030.00200030004
	fmt.Println(a*b + a*c) // 200030.0020003
}

$ a \times b + a \times c$ の方が誤差が小さくなる。
乗算と除算は、加算・減算より先に行うことで、誤差を小さくすることができる。

まとめ

  • Goのfloat32, float64は近似値をとるので、誤差が発生する
  • 加算・減算は同じ桁数の演算をまとめて行うことで、精度が上がる
  • 四則演算が混合する場合、乗算と除算を先に行うことで、精度が上がる
2
3
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
2
3