Save 37% off PRO during our Black Friday Sale! »

Floating Point Numbers and Decimal in Go

3f97ef294ccbc5e3f16b7594e06fd1f3?s=47 Ryoya Sekino
November 13, 2021

Floating Point Numbers and Decimal in Go

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

3f97ef294ccbc5e3f16b7594e06fd1f3?s=128

Ryoya Sekino

November 13, 2021
Tweet

Transcript

  1. Floating Point Numbers and Decimal in Go Ryoya Sekino @Go

    Conference 2021 Autumn
  2. 本セッションの概要 ゴール Go で小数の計算を行う際の注意点と取りうるアプロー チを理解すること 話すこと プログラム一般の小数の仕組み Go の組み込みの文法での小数の挙動 Go

    で小数計算を行うときのアプローチ 1. Introduction 2
  3. Agenda 1. Introduction 2. 小数と計算誤差 3. Go の組み込みの小数の挙動 4. Go

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

    の小数計算のアプローチ 5. まとめ 1. Introduction 4
  5. $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
  6. Briefly about UPSIDER 成長企業向けの法人カードを提供しているスタートア ップです 金融SaaS として企業のお金周りの課題を解決すること を目指しています Tech Stacks:

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

    の小数計算のアプローチ 5. まとめ 2. 小数と計算誤差 7
  8. 小数のプログラミングでの注意点 日常・数学での小数とは異なる点がある 1. 内部的には2 進数( ビット列) で表現される 2. 浮動小数点数が一般的に使用される 2.

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

    になるはずが。。。 func main() { a := 0.1 b := 0.3 fmt.Println(a*3 == b) } 2. 小数と計算誤差 9
  10. 小数計算のプログラムは誤差を起こしうる 下記のコードを見てみましょう 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
  11. 期待した数字と実際の 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
  12. 2 進数で表現できない 10 進数小数がある 10 進数小数には、2 進数で表現できない値がある 2. 小数と計算誤差 12

  13. 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
  14. 2 進数で表現できる値の場合 10 進数 0.625 -> 2 進数 0.101 func

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

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

    2. 1 の結果の整数部の数字をその桁の数とする 2. 小数と計算誤差 16
  17. 2 進数で表現できる値 最終的に小数部が0 になる 10 進数 0.625 -> 2 進数

    0.101 2. 小数と計算誤差 17
  18. 2 進数で表現できない値 循環小数となって終わらない 10 進数 0.6 -> 2 進数 0.10011001.....

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

    小数部 0.234 処理が高速 浮動小数点数 数値をビット列の指数表記で表現する イメージ: 0.01234 = 1.234 * 10^-2 表現できる数の幅が多い プログラムでは一般的 2. 小数と計算誤差 19
  20. 固定小数点数の構造 ビット列の中の小数点の位置が決まっている 表現できる整数、小数の桁数が決まっている int は、小数部を0 にした固定小数点数といえる 処理が早い 2. 小数と計算誤差 20

  21. 浮動小数点数の構造 ビット列を、 符号部 + 指数部 + 仮数部 にわける 仮数部: 実際の数値を、整数1

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

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

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

    した32bit, 64bit の浮動小数点数となっている = 123.625 3. Go の組み込みの小数の挙動 24
  25. 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
  26. 定数にすると挙動が変わる 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
  27. 定数は型を持たないで存在することができ る 型を持たないため、型の制約を受けない ( 厳密には、値に応じて、 default type が存在して、変 数への代入時等に使用される) 具体的な変数への代入等した際に、型を持った値に変

    換される( そのため、代入時にoverflow したりする) func main() { const a = 111111111111111111111 var _ = a } $ go run main.go constant 111111111111111111111 overflows int 3. Go の組み込みの小数の挙動 27
  28. 型を持たない定数の数値は任意の精度で計 算してくれる ただのふつうの数として扱われる https://go.dev/blog/constants 仕様上は精度の限界がない( 処理系に依存する) 3. Go の組み込みの小数の挙動 Numeric

    constants live in an arbitrary-precision numeric space; they are just regular numbers. “ “ 28
  29. 型なし定数は大きい数の定義に使える 例えば、円周率を高めの精度で定義しておける Pi = 3.14159265358979323846264338327950288419716939937510582097494459 https://pkg.go.dev/math#pkg-constants 3. Go の組み込みの小数の挙動 29

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

    の小数計算のアプローチ 5. まとめ 2. 小数と計算誤差 30
  31. Go で小数計算を行う際にはいくつかのアプ ローチがある 1. 整数に直す(Basis Point) 2. math/big Float 3.

    math/big Rat 4. shopspring/decimal 4. Go の小数計算のアプローチ 31
  32. 整数に変換することで、小数計算の問題を 回避することができる 元の数値を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
  33. ユースケース次第では整数に変換するのが わかりやすい 桁数が限られている場合に有用 特に1 万分の1 を単位とするものを、万分率(Basis Point) という 万分率用の外部パッケージもある https://github.com/mercari/go-bps

    ただし、指定の桁以下の値の扱い( 切り上げ、切り捨 て、四捨五入) に注意する必要がある 4. Go の小数計算のアプローチ - 1. 整数に直す(Basis Point) 33
  34. 整数に直す方法が適さない場合もある 桁数が多かったり、可変の場合など 仮想通貨、為替変換...etc パッケージを利用しない限りは、自前で実装しないと いけないので、面倒ではある 4. Go の小数計算のアプローチ - 1.

    整数に直す 34
  35. math/big を使うとより高い精度で数を扱え る 以下の三つの type が用意されている Int: 多倍長整数 Float: 多倍長浮動小数点数

    Rat: 有理数( 分数) 4. Go の小数計算のアプローチ - math/big 35
  36. 精度を一定上げられれば良いなら、 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
  37. 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
  38. 分数で扱えば計算誤差もない 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
  39. ただし big.Rat にも注意点がある 1. 小数で表現しきれない分数の取り扱い 2. 関数の操作が独特 4. Go の小数計算のアプローチ

    - 3. math/big Rat 39
  40. 小数で表現しきれない分数の取り扱いに注 意が必要 例) 小数で割り切れない分数で計算すると端数が消滅する 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
  41. 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
  42. big.Rat は単純な割り算・掛け算の計算に限 定した方が良さそう 単純な手数料や税金の計算などでは、使いやすく問題 ない 足し算・引き算や分配の計算では、小数に直す際に誤 差が起きうるので、注意が必要 4. Go の小数計算のアプローチ

    - 3. math/big Rat 42
  43. shopspring/decimal でより安全に小数を扱 う 最大で2 の31 乗桁まで小数を誤差なく扱える 内部では、int(big.Int) の指数表記で持っている( つま り、整数の指数表記に直している)

    math/big のような独特なAPI をしていない 組み込みの型やJSON, XML などとの相互変換の関数も 提供されている https://github.com/shopspring/decimal 4. Go の小数計算のアプローチ - 3.shopspring/decimal 43
  44. 計算誤差も出ない 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
  45. 内部を全部 decimal ・外部を全部 string にす るアプローチ 小数を含みうる値をGo のコードではすべてdecimal で扱 い、API

    のIO やDB ではすべてstring にする 安全かつ平易で間違えづらい ただし、文字列変換・エラーハンドリングの手間とパ フォーマンスが許容する必要はある 4. Go の小数計算のアプローチ - 3.shopspring/decimal 45
  46. パフォーマンスを比較してみた 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
  47. 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
  48. 結果 : 整数に直す方法が一番早かった 早い順 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
  49. 4 章のまとめ 4. Go の小数計算のアプローチ 49

  50. まとめ プログラミングの小数の扱いには注意が必要 Go のfloat も一般的な浮動小数点のため、計算誤差を起 こす 型なし定数という特殊な状態もある Go で小数計算を行う際は、ユースケースに応じて以下 のどれかを選んだ方が良い

    整数に直して計算する math/big パッケージを利用する shopspring/decimal パッケージを利用する 5. まとめ 50
  51. 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
  52. Reference Go 言語の浮動小数点数のお話 https://shogo82148.github.io/blog/2017/10/28/golang-floating- point-number/ 料率計算における小数点数の扱いについて https://engineering.mercari.com/blog/entry/20201203-basis- point/ 入門Go 言語仕様

    / Go Specification Untyped Constants https://speakerdeck.com/dqneo/go-specification-untyped- constants 52
  53. EOF