浮動小数点
Goには2つの浮動小数点型がある。
- float32
- float64
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つの浮動小数点数は以下のように作成される。
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は近似値をとるので、誤差が発生する
- 加算・減算は同じ桁数の演算をまとめて行うことで、精度が上がる
- 四則演算が混合する場合、乗算と除算を先に行うことで、精度が上がる