Go
golang

1を1億回足して1億にならない場合

More than 1 year has passed since last update.

まぁ浮動小数点型の仕様を知れば当たり前の話なのだが,面白そうなので「1を1億回足す」ってのを Go 言語でも書いてみる。

loop1.go
package main

import "fmt"

func main() {
    var d float32 = 0.0
    for i := 0; i < 100000000; i++ {
        d += 1.0
    }
    fmt.Println(d)
}

実行結果は予想通り

$ go run loop1.go
1.6777216e+07

となる1。念のため float64 でも試してみよう。

loop2.go
package main

import "fmt"

func main() {
    var d float64 = 0.0
    for i := 0; i < 100000000; i++ {
        d += 1.0
    }
    fmt.Println(d)
}

結果は

$ go run loop2.go
1e+08

で,ちゃんと1億になる。 Go 言語では基本型のサイズが厳密に決まってるので,浮動小数点型の計算誤差についてもきちんと見積もれるはずである。

ちなみに

loop3.go
package main

import "fmt"

func main() {
    for d := 0.0; d < 1.0; d += 0.1 {
        fmt.Println(d)
    }
}

とすると2

$ go run loop3.go
0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

ってなことになる3 ので浮動小数点型の変数をループカウンタにするのは止めましょうね。約束だよ!

ブックマーク



  1. float32 は32ビットサイズの浮動小数点数型で,符号部1ビット,指数部8ビット,仮数部23ビット,という内訳になっている(仮数部は仮数の小数点以下を表す)。つまり有効桁数が24ビット(10進数で約7桁)しかない。したがって今回のような「1づつ加算する動作を繰り返す」処理では16,777,216(=0xffffff+1)以降は「情報落ち」が発生する。ちなみに float64 は64ビットサイズで仮数部は52ビットあり,10進数にして約15桁の有効桁数になる。 

  2. d := 0.0” と記述した場合,変数 dfloat64 として宣言・初期化される。厳密には定数 “0.0” は,いったん「型付けなし」の浮動小数点数として評価された後,変数宣言時に float64 に暗黙的に変換される。 Go 言語におけるこの定数の機能は何かと便利なので覚えておくとよいだろう。 

  3. このような結果になるのは float32/float64 の浮動小数点数型の内部表現が2進数になっているため。たとえば 0.1 を2進数で表すと「0.000110011...」と循環しキリのいい値にならない。このため 0.1 を加算していくと「丸め誤差」が蓄積していくのである。