Help us understand the problem. What is going on with this article?

Better C - Goと小数

はじめに

昨年のGo Advent Calenderでは、Better C - Goと整数 #golangという記事を書きました。

今年は、小数について書きたいと思います。

Goでは小数のために、浮動小数点数の float32, float64math.BitFloat が用意されています。固定小数点数は存在しません。
float32, float64 はIEEE 754で定義されている仕様に従うため、Go特有の挙動というのは少ないですが、少しだけbetterCの点も紹介します。

IEEE 754-2008は、こちらから$97で購入できます。
http://standards.ieee.org/findstds/standard/754-2008.html

シフト演算

floatでシフト演算はコンパイルエラー

var a float64 = 100
b := a << 1
fmt.Printf("%f", b)

実行結果

main.go:9:9: invalid operation: a << 1 (shift of type float64)

https://play.golang.org/p/yBz3uKD34h

繰り上げ、切り捨て

mathパッケージの Ceil(),Floor(), Trunc()メソッド。

    var a float64 = 100.12345
    var b float64 = -100.12345

    fmt.Printf("a = %f\n", a)
    fmt.Printf("b = %f\n\n", b)

    fmt.Println("Ceil 引数以上のもっとも小さい整数")
    fmt.Printf("ceil a %f\n", math.Ceil(a))
    fmt.Printf("ceil b %f\n\n", math.Ceil(b))

    fmt.Println("Floor 引数以下のもっとも大きい整数")
    fmt.Printf("floor a %f\n", math.Floor(a))
    fmt.Printf("floor b %f\n\n", math.Floor(b))

    fmt.Println("Trunc 小数点以下の切り捨て")
    fmt.Printf("trunc a %f\n", math.Trunc(a))
    fmt.Printf("trunc b %f\n\n", math.Trunc(b))

実行結果

a = 100.123450
b = -100.123450

Ceil 引数以上のもっとも小さい整数
ceil a 101.000000
ceil b -100.000000

Floor 引数以下のもっとも大きい整数
floor a 100.000000
floor b -101.000000

Trunc 小数点以下の切り捨て
trunc a 100.000000
trunc b -100.000000

https://play.golang.org/p/R6ZE8L-7dI

四捨五入

Round関数はGo1.10以降で使うことができます。

a = 100.123450
b = 100.523450
c = -100.123450
d = -100.523450

Round 四捨五入
round a 100.000000
round b 101.000000
round c -100.000000
round d -101.000000

https://play.golang.org/p/bQ7GpvrTwNF

ゼロ除算

intの場合はpanicになります。

zero := 0
fmt.Printf("ゼロ除算 %v\n", 1/zero)

実行結果

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.main()
    /tmp/sandbox853689335/main.go:9 +0x20

https://play.golang.org/p/QSCjyp49Rm

NaN(非数)

NaN。Not a numberの略。

-1の平方根のような負の数の平方根は実数の世界では表現できない虚数であるためNaNとなる。

nan := math.Sqrt(-1)
fmt.Printf("%v\n", nan)
fmt.Printf("== での比較ではtrueにならない %v\n", nan == math.NaN())
fmt.Printf("math.IsNaN()で判定する %v\n", math.IsNaN(nan))
NaN
NaN同士の比較は == ではtrueにならない false
math.IsNaN()で判定する true

https://play.golang.org/p/4hL5KumMA5

Inf(無限)

Inf。Infinityの略。

float64で表現できる値域を超える場合にInfとなる。

inf_plus := math.Pow(0, -1)
fmt.Printf("%v\n", inf_plus)
fmt.Printf("== での比較 %v\n", inf_plus == math.Inf(1))
fmt.Printf("math.IsInf(inf_plus 1)で判定する %v\n", math.IsInf(inf_plus, 1))
fmt.Printf("math.IsInf(inf_plus, -1)で判定する %v\n\n", math.IsInf(inf_plus, -1))

inf_minus := math.Log(0)
fmt.Printf("%v\n", inf_minus)
fmt.Printf("== での比較 %v\n", inf_minus == math.Inf(-1))
fmt.Printf("math.IsInf(inf_minus 1)で判定する %v\n", math.IsInf(inf_minus, 1))
fmt.Printf("math.IsInf(inf_minus, -1)で判定する %v\n", math.IsInf(inf_minus, -1))

https://play.golang.org/p/rMcJ1N3oOz

ゼロ除算でpanicにならない場合

ゼロをゼロで除算した場合は、panicではなくNaNになる。

var zero float64 = 0
fmt.Printf("0/0= %v\n", zero/zero)

実行結果

0/0= NaN

https://play.golang.org/p/bjm8l5bQx1

誤差

打ち切り誤差

10進数で割り切れず計算が終わらないものを途中で打ち切る場合の誤差。
循環小数や円周率など。

loop := 1.0 / 3
fmt.Printf("%f\n", loop)

実行結果

//  本来は無限に続くが切り捨てられる
0.333333

https://play.golang.org/p/sgRX0hKgHp

桁落ち誤差

丸め誤差がある値同士を減算した場合に発生する有効桁数の減少による誤差。

root(1001) - root(999)を計算する場合。

a := math.Sqrt(1001)
b := math.Sqrt(999)
fmt.Printf("a = %f\n", a)
fmt.Printf("b = %f\n", b)
fmt.Printf("a - b = %f\n", a-b)

実行結果

a = 31.638584
b = 31.606961
a - b = 0.031623

https://play.golang.org/p/nI-D1CO-X8

31.63858403911274914310629158480098308708005351898025493377 - 31.60696125855821654520421398569900243024310197917304499132

有効桁数8桁で考えると、正しい答えは0.031622780なので、誤差は0.00000022 です。
計算前の値の有効桁数は8桁ですが、計算結果は有効桁数が5桁になってしまい大きな精度の差がでています。

http://www.wolframalpha.com/input/?i=root(1001)+-+root(999)

情報落ち誤差

// 桁数 15桁
    var a1 float64 = 1000000000000000
    var b float64 = 1

    fmt.Printf("a1 = %f\n", a1)
    fmt.Printf("b = %f\n", b)
    fmt.Printf("a1 + b = %f\n\n", a1+b)

    // 桁数 16桁
    var a2 float64 = 10000000000000000
    fmt.Printf("a2 = %f\n", a2)
    fmt.Printf("b = %f\n", b)
    fmt.Printf("a2 + b = %f\n", a2+b)

実行結果

a2では足したはずの1が消えます。

a1 = 1000000000000000.000000
b = 1.000000
a1 + b = 1000000000000001.000000

a2 = 10000000000000000.000000
b = 1.000000
a2 + b = 10000000000000000.000000

https://play.golang.org/p/Dt2NJQQ1VQ

丸め誤差

丸め誤差とは、10進数では割り切れても0.1などのように2進数で表現したときに無限に繰り返してしまう値を、有限で切り上げてしまうことにより正確な値が表現できないために発生する誤差です。

var a float64 = 0.1
var b float64 = 0.2
var c float64 = 0.3
var trueOrFalse = a+b == c
fmt.Printf("a(%f)+b(%f) == c(%f) -> %v\n", a, b, c, trueOrFalse)

実行結果

a(0.100000)+b(0.200000) == c(0.300000) -> false

https://play.golang.org/p/E3nx0PEND1

誤差が発生しない特殊ケース

小数の計算でも誤差が発生しないケースとして、定数で宣言した場合がある。

fmt.Printf("%v\n", 0.1+0.2==0.3)

実行結果

true

https://play.golang.org/p/eHqlwriSBi

このような結果になる理由は、Better C - Goと整数 #golangでも紹介しましたが、定数は任意の精度で正確な値を表すためです。

Constants
Numeric constants represent exact values of arbitrary precision and do not overflow.
定数
数値定数は任意の精度の正確な値を表し、オーバーフローしません。
https://golang.org/ref/spec#Constants

最大値と最小値

max := math.MaxFloat64
min := -math.MaxFloat64

fmt.Printf("float64の最小値(%v)と最大値(%v)\n\n", min, max)
fmt.Printf("float64の最小値への減算 誤差のため最小値と変わらない = %v\n", min-1000000000000000000000000000000000000000000000000000000000000000000000)
fmt.Printf("float64の最小値同士の加算 = %v\n\n", min+min)

fmt.Printf("float64の最大値への加算 誤差のため最大値と変わらない = %v\n", max+1000000000000000000000000000000000000000000000000000000000000000000000)
fmt.Printf("float64の最大値同士の加算 = %v\n", max+max)

実行結果

float64の最小値(-1.7976931348623157e+308)と最大値(1.7976931348623157e+308)

float64の最小値への減算 誤差のため最小値と変わらない = -1.7976931348623157e+308
float64の最小値同士の加算 = -Inf

float64の最大値への加算 誤差のため最大値と変わらない = 1.7976931348623157e+308
float64の最大値同士の加算 = +Inf

https://play.golang.org/p/bRDMM69th6

キャスト

非定数の数値の変換には、下の規則が適用されます。

  1. 浮動小数点数を整数に変換するとき、小数部は捨てられます。(ゼロに近づくよう切り捨て/切り上げ)
  2. 整数、または浮動小数点の値を浮動小数点型に、もしくは複素数の値を他の複素数型に変換するときは、結果となる値はその変換先の型で規定されている精度に丸められます。たとえば、float32型の変数xの値は、格納されるときIEEE-754の32ビットの数値以上の精度が使われます。しかしfloat32(x)が表すのは32ビットに丸められたxの値です。同様に、x + 0.1は32ビット以上の精度が使われますが、float32(x + 0.1)はそうなりません。

浮動小数点値または複素数を含んでいるすべての非定数変換において、結果となる型が値を表現することができなければ、変換自体は成功しますが、結果となる値は実装依存となります。

https://www.jpcert.or.jp/sc-rules/c-flp06-c.html

var a float64 = 1.1
var b int64 = int64(a)
fmt.Printf("float64からin64へのキャスト %v\n", b)

実行結果

float64からin64へのキャスト 1

https://play.golang.org/p/Kv8sY_Sfud

var a uint64 = 100
var b float64 = float64(a)

// 値の範囲内であれば問題ない
fmt.Printf("uint64からfloat64へのキャスト %v\n", b)

実行結果

uint64からfloat64へのキャスト 100

https://play.golang.org/p/bLwQkfWmup

次の違反コード例では、return 文において式の結果をキャストしておらず、返り値の範囲や精度が想定を超えないことを保証していない。この不確定さは、定数 0.1f の使用がもたらすものである。定数は、float より大きい範囲もしくは精度を持つ値として格納される可能性がある。それゆえ、x * 0.1f の結果も、float より大きい範囲または精度を持ちうる。前述のとおり、この範囲や精度は float の範囲や精度に変換できない可能性があるため、calcPercentage() の呼出し元は想定よりも精度の高い値を受け取る可能性がある。これは、プラットフォームによってプログラムの動作が異なる結果となる可能性がある。
https://www.jpcert.or.jp/sc-rules/c-flp07-c.html

違反コード(C言語)

float calc_percentage(float value) {
  return value * 0.1f;
}

void float_routine(void) {
  float value = 99.0f;
  long double percentage;

  percentage = calc_percentage(value);
}

適合コード(Go)

Goは定数に対して型を明示しない場合にも適切な型になるため問題がない。

func calc_percentage(value float32) float32 {
    return value * 0.1
}

func main() {
    var value float32 = 99.0
    var percentage float64 = float64(calc_percentage(value))
    fmt.Printf("%v\n", percentage)
}

https://play.golang.org/p/lEC0cSCtoR

比較

> 等価演算子である==と!=は、「比較可能」なオペランドに対して使用します。順序演算子である<、<=、>、>=は「有順序」なオペランドに対して使用します。これらの用語および比較結果は次のように定義されています。

浮動小数点値は「比較可能」かつ「有順序」であり、IEEE-754の規定に従います。

浮動小数点数変数をループカウンタに使用しない

C言語の仕様と少し違う点を紹介。

C言語では、以下の理由でループカウンタとして浮動小数点数を使うと、ループ回数が処理系によって異なる可能性がある。

以下の違反コード例では、ループカウンタとして浮動小数点数変数を使っている。10進小数 0.1 は2進数では循環小数になるので、2進の浮動小数点数として正確に表現することができない。処理系によって異なるが、ループは 9 回あるいは 10 回繰り返される。
処理系によって精度の限界は異なる。コードの可搬性を維持するためには、ループカウンタに浮動小数点数変数を使ってはいけない。
https://www.jpcert.or.jp/sc-rules/c-flp30-c.html

違反コード(C言語)

  for (float x = 0.1f; x <= 1.0f; x += 0.1f) {
    /* ループは9回あるいは10回繰り返す */
  }

適合コード(Go)

Goでは、浮動小数点数では表現仕切れない場合に近似値になることが仕様として決まっているためループカウンタの回数は一定となる。
しかしそもそも浮動小数点数をループカウンタとして使うのはやめたほうがいいと思います。

    var i int = 0
    for x := 0.1; x <= 1.0; x += 0.1 {
        i++
        fmt.Printf("%v: %v\n", i, x)
    }

実行結果

1: 0.1
2: 0.2
3: 0.30000000000000004
4: 0.4
5: 0.5
6: 0.6
7: 0.7
8: 0.7999999999999999
9: 0.8999999999999999
10: 0.9999999999999999

https://play.golang.org/p/5cOGh9blau

まとめ

ほとんど一般的な話でしたが、よいGoライフを!

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away