Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Floating Point Numbers and Decimal in Go

Ryoya Sekino
November 13, 2021

Floating Point Numbers and Decimal in Go

Go Conference 2021 Autumnでの登壇資料です。Goでの小数計算の問題と対象方法について、プログラミング一般の小数問題から解説しています。

Ryoya Sekino

November 13, 2021
Tweet

More Decks by Ryoya Sekino

Other Decks in Programming

Transcript

  1. Agenda 1. Introduction 2. 小数と計算誤差 3. Go の組み込みの小数の挙動 4. Go

    の小数計算のアプローチ 5. まとめ 1. Introduction 3
  2. Agenda 1. Introduction 2. 小数と計算誤差 3. Go の組み込みの小数の挙動 4. Go

    の小数計算のアプローチ 5. まとめ 1. Introduction 4
  3. $whoami Ryoya Sekino / 関野 涼也 Software Engineer at UPSIDER,

    inc., leading Card Processing team Writing Go for 2.5 years as a main language Spoke at Go Conference 2021 Spring as well Loves music, DJ’ing and bathhouse/sauna 1. Introduction 5
  4. Briefly about UPSIDER 成長企業向けの法人カードを提供しているスタートア ップです 金融SaaS として企業のお金周りの課題を解決すること を目指しています Tech Stacks:

    Go, Kotlin, TypeScript, k8s, Istio, microservices,etc. 直近で38 億円の資金調達もして、絶賛採用中です!! 1. Introduction Gopher の原著作者はRenée French さんです。以降のページも同様。 6
  5. Agenda 1. Introduction 2. 小数と計算誤差 3. Go の組み込みの小数の挙動 4. Go

    の小数計算のアプローチ 5. まとめ 2. 小数と計算誤差 7
  6. 小数計算のプログラムは誤差を起こしうる 下記のコードを見てみましょう 0.1 * 3 == 0.3 数学(10 進数) 的にはtrue

    になるはずが。。。 func main() { a := 0.1 b := 0.3 fmt.Println(a*3 == b) } 2. 小数と計算誤差 9
  7. 小数計算のプログラムは誤差を起こしうる 下記のコードを見てみましょう 0.1 * 3 == 0.3 数学(10 進数) 的にはtrue

    になるはずが。。。 func main() { a := 0.1 b := 0.3 fmt.Println(a*3 == b) } 結果はfalse $ go run main.go false 2. 小数と計算誤差 10
  8. 期待した数字と実際の float の値が異なる func main() { a := 0.1 b

    := 0.3 // 小数第25 位まで出力 fmt.Printf("%.25f\n", a*3) fmt.Printf("%.25f\n", b) fmt.Printf("%.25f\n", a*3-b) } $ go run main.go 0.3000000000000000444089210 0.2999999999999999888977698 0.0000000000000000555111512 2. 小数と計算誤差 11
  9. Go で小数の 10 進数を 2 進数に変換してみる func printFloatAsBiStr(v float64, digit

    int) { // 整数にして計算する shift := math.Pow10(digit) remain := v * shift fmt.Print("0.") for { // 1. 2 倍する double := remain * 2 // 2. 1 桁目を取得する ( その2 進数での桁の値) val := math.Floor(double / shift) fmt.Print(val) // 3. 1 桁目を取り除く remain = double - val*shift // 残りが0 になるまで1~3 を繰り返す if remain == 0 { break } } } 2. 小数と計算誤差 ※ 上記のコードは整数には対応してません 13
  10. 2 進数で表現できる値の場合 10 進数 0.625 -> 2 進数 0.101 func

    main() { printFloatAsBiStr(0.625, 3) } $ go run main.go 0.101 2. 小数と計算誤差 14
  11. 2 進数で表現できない値の場合 10 進数 0.6 -> 2 進数では循環小数になる func main()

    { printFloatAsBiStr(0.6, 1) } $ go run main.go 0.1001100110011001100110011001100110011001100110011001100110011... 2. 小数と計算誤差 15
  12. 小数の 10 進数から 2 進数への変換方法 小数部が0 になるまで、下記を繰り返す 1. 小数部を2 倍する

    2. 1 の結果の整数部の数字をその桁の数とする 2. 小数と計算誤差 16
  13. 小数の表現方法には、二種類ある 固定小数点数 数値をビット列で表現する際に、あらかじめて整数部 と小数部が決まっている イメージ: 1.234 = 整数部 1 +

    小数部 0.234 処理が高速 浮動小数点数 数値をビット列の指数表記で表現する イメージ: 0.01234 = 1.234 * 10^-2 表現できる数の幅が多い プログラムでは一般的 2. 小数と計算誤差 19
  14. 浮動小数点数の構造 ビット列を、 符号部 + 指数部 + 仮数部 にわける 仮数部: 実際の数値を、整数1

    桁から始まる数値にず らしたもの ex) 0.123, 123 -> 仮数1.23 指数部: 仮数を何桁ずらすと実際の数値になるか 2. 小数と計算誤差 21
  15. プログラムの小数では、浮動小数点の方が 使われることが多い 表現できる幅が固定小数点数よりはるかに大きい Go の float32 , float64 も、32bit, 64bit

    の浮動小数点数 2 進数にしたときに、浮動小数点数の仮数部がはみ出す 数字で計算誤差が起きる 2. 小数と計算誤差 22
  16. Agenda 1. Introduction 2. 小数と計算誤差 3. Go の組み込みの小数の挙動 4. Go

    の小数計算のアプローチ 5. まとめ 3. Go の組み込みの小数の挙動 23
  17. float32 と float64 の構造 float32 , float64 は、それぞれ、IEEE 754 の規格に準拠

    した32bit, 64bit の浮動小数点数となっている = 123.625 3. Go の組み込みの小数の挙動 24
  18. float64 の有効桁数は 10 進数で 15 桁程度 func main() { a

    := 0.1 b := 0.3 fmt.Printf("a*3 == b: %t\n", a*3 == b) fmt.Printf("a*3 - b = %v\n", a*3-b) // 指数表示になる } $ go run main.go a*3 == b: false a*3 - b = 5.551115123125783e-17 3. Go の組み込みの小数の挙動 25
  19. 定数にすると挙動が変わる func main() { const a = 0.1 const b

    = 0.3 fmt.Printf("a*3 == b: %t\n", a*3 == b) fmt.Printf("a*3 - b = %v\n", a*3-b) } !!! $ go run main.go a*3 == b: true a*3 - b = 0 3. Go の組み込みの小数の挙動 26
  20. 定数は型を持たないで存在することができ る 型を持たないため、型の制約を受けない ( 厳密には、値に応じて、 default type が存在して、変 数への代入時等に使用される) 具体的な変数への代入等した際に、型を持った値に変

    換される( そのため、代入時にoverflow したりする) func main() { const a = 111111111111111111111 var _ = a } $ go run main.go constant 111111111111111111111 overflows int 3. Go の組み込みの小数の挙動 27
  21. Agenda 1. Introduction 2. 小数と計算誤差 3. Go の組み込みの小数の挙動 4. Go

    の小数計算のアプローチ 5. まとめ 2. 小数と計算誤差 30
  22. 整数に変換することで、小数計算の問題を 回避することができる 元の数値を10 のN 乗分かけて、整数で計算する 計算結果に10 のマイナスN 乗かけたものが、最終的な値 となる イメージ

    price := 12345 rate := 0.02 // インプットを100 倍して計算する shift := math.Pow10(2) rateInt := int(rate * shift) point := price * rateInt // 100 分の1 する( 端数は四捨五入) fmt.Printf("point: %v\n", math.Round(float64(point)/shift)) 4. Go の小数計算のアプローチ - 1. 整数に直す(Basis Point) 32
  23. ユースケース次第では整数に変換するのが わかりやすい 桁数が限られている場合に有用 特に1 万分の1 を単位とするものを、万分率(Basis Point) という 万分率用の外部パッケージもある https://github.com/mercari/go-bps

    ただし、指定の桁以下の値の扱い( 切り上げ、切り捨 て、四捨五入) に注意する必要がある 4. Go の小数計算のアプローチ - 1. 整数に直す(Basis Point) 33
  24. 精度を一定上げられれば良いなら、 big.Float が使える float64 よりも精度の高い( 仮数の大きい) 浮動小数点となる func main() {

    a := 0.1 fmt.Println(a * a) b := new(big.Float).SetPrec(1024).SetFloat64(a*a) fmt.Println(b) } $ go run main.go 0.010000000000000002 0.010000000000000001942890293094023945741355419158935546875 4. Go の小数計算のアプローチ - 2. math/big Float 36
  25. big.Rat で分数で数値を扱える func main() { x := big.NewRat(1, 10) //

    0.1 y := big.NewRat(3, 1) // 3 r := new(big.Rat).Mul(x, y) // 0.1 * 3 f, _ := r.Float64() fmt.Println(f) } $ go run main.go 0.3 4. Go の小数計算のアプローチ - 3. math/big Rat 37
  26. 分数で扱えば計算誤差もない x := big.NewRat(1, 10) // 0.1 y := new(big.Rat).SetInt64(3)

    // 3 z := big.NewRat(3, 10) // 0.3 r := new(big.Rat).Mul(x, y) // 0.1 * 3 fmt.Println(r.Cmp(z) == 0) // 等しければtrue $ go run main.go true 4. Go の小数計算のアプローチ - 3. math/big Rat 38
  27. 小数で表現しきれない分数の取り扱いに注 意が必要 例) 小数で割り切れない分数で計算すると端数が消滅する func main() { total := new(big.Rat).SetInt64(1)

    // 1 oneThird := big.NewRat(1, 3) // 1/3 remain := total.Sub(total, oneThird) // 1 - 1/3 - 1/3 - 1/3 = 0 .Sub(total, oneThird) .Sub(total, oneThird) fmt.Printf("1 - %s - %s -%s = %s", oneThird.FloatString(3), oneThird.FloatString(3), oneThird.FloatString(3), remain.FloatString(3), ) } $ go run main.go 1 - 0.333 - 0.333 -0.333 = 0.000 4. Go の小数計算のアプローチ - 3. math/big Rat 40
  28. math/big の関数が独特で事故を起こしやす い 計算関数のレシーバも変更される func main() { x := big.NewRat(1,

    10) // 0.1 y := new(big.Rat).SetInt64(3) // 3 r := x.Mul(x, y) // x も更新される fmt.Printf("%v * %v = %v", x, y, r) } x も上書きされている $ go run main.go 3/10 * 3/1 = 3/10 4. Go の小数計算のアプローチ - 3. math/big Rat 41
  29. shopspring/decimal でより安全に小数を扱 う 最大で2 の31 乗桁まで小数を誤差なく扱える 内部では、int(big.Int) の指数表記で持っている( つま り、整数の指数表記に直している)

    math/big のような独特なAPI をしていない 組み込みの型やJSON, XML などとの相互変換の関数も 提供されている https://github.com/shopspring/decimal 4. Go の小数計算のアプローチ - 3.shopspring/decimal 43
  30. 計算誤差も出ない func main() { a, _ := decimal.NewFromString("0.1") b :=

    decimal.NewFromInt(3) c, _ := decimal.NewFromString("0.3") fmt.Printf("a * b = %v\n", a.Mul(b)) fmt.Printf("c: %v\n", c) fmt.Printf("a * b == c: %t\n", a.Mul(b).Equal(c)) } $ go run main.go a * b = 0.3 c: 0.3 a * b == c: true 4. Go の小数計算のアプローチ - 3.shopspring/decimal 44
  31. 内部を全部 decimal ・外部を全部 string にす るアプローチ 小数を含みうる値をGo のコードではすべてdecimal で扱 い、API

    のIO やDB ではすべてstring にする 安全かつ平易で間違えづらい ただし、文字列変換・エラーハンドリングの手間とパ フォーマンスが許容する必要はある 4. Go の小数計算のアプローチ - 3.shopspring/decimal 45
  32. パフォーマンスを比較してみた float で受け取った数字を足し算してstring に戻す関数 func AddByShift(a, b float64, shiftDigit int)

    string { shift := math.Pow10(shiftDigit) ai := int(a * shift) bi := int(b * shift) return strconv.FormatFloat((float64(ai+bi) / (shift)), 'f', shiftDigit, 64) } func AddByBigFloat(a, b float64, prec int) string { af := new(big.Float).SetFloat64(a) bf := new(big.Float).SetFloat64(b) return new(big.Float).SetPrec(uint(prec)).Add(af, bf).Text('f', prec) } func AddByRat(a, b float64, prec int) string { ar := new(big.Rat).SetFloat64(a) br := new(big.Rat).SetFloat64(b) return ar.Add(ar, br).FloatString(prec) } func AddByDecimal(a, b float64) string { ad := decimal.NewFromFloat(a) bd := decimal.NewFromFloat(b) return ad.Add(bd).String() } 4. Go の小数計算のアプローチ 46
  33. Benchmark Test funcs func BenchmarkAddByShift(b *testing.B) { for i :=

    0; i < b.N; i++ { _ = AddByShift(0.12, 0.34, 2) } } func BenchmarkAddByBigFloat(b *testing.B) { for i := 0; i < b.N; i++ { _ = AddByBigFloat(0.12, 0.34, 256) } } func BenchmarkAddByRat(b *testing.B) { for i := 0; i < b.N; i++ { _ = AddByRat(0.12, 0.34, 30) } } func BenchmarkAddByDecimal(b *testing.B) { for i := 0; i < b.N; i++ { _ = AddByDecimal(0.12, 0.34) } } 4. Go の小数計算のアプローチ 47
  34. 結果 : 整数に直す方法が一番早かった 早い順 1. 整数に直す 2. big.Float (※) 3.

    decimal 4. big.Rat cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkAddByShift-12 5712213 208.5 ns/op 28 B/op 2 allocs/op BenchmarkAddByBigFloat-12 1248880 949.4 ns/op 784 B/op 11 allocs/op BenchmarkAddByDecimal-12 964044 1319 ns/op 192 B/op 11 allocs/op BenchmarkAddByRat-12 687622 1967 ns/op 1104 B/op 37 allocs/op ( 他の四則演算で試しても、ほぼ同等の結果だった) ( big.Float は、精度を上げるともっと遅くなる) 4. Go の小数計算のアプローチ 48
  35. Reference math package https://pkg.go.dev/math shopspring/decimal package https://pkg.go.dev/github.com/shopspring/decimal The Go Programming

    Language Specification https://golang.org/ref/spec The Go Blog Constants https://go.dev/blog/constants 754-2019 - IEEE Standard for Floating-Point Arithmetic https://ieeexplore.ieee.org/document/8766229 51
  36. EOF