Slide 1

Slide 1 text

Floating Point Numbers and Decimal in Go Ryoya Sekino @Go Conference 2021 Autumn

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

$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

Slide 6

Slide 6 text

Briefly about UPSIDER 成長企業向けの法人カードを提供しているスタートア ップです 金融SaaS として企業のお金周りの課題を解決すること を目指しています Tech Stacks: Go, Kotlin, TypeScript, k8s, Istio, microservices,etc. 直近で38 億円の資金調達もして、絶賛採用中です!! 1. Introduction Gopher の原著作者はRenée French さんです。以降のページも同様。 6

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

小数計算のプログラムは誤差を起こしうる 下記のコードを見てみましょう 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

Slide 11

Slide 11 text

期待した数字と実際の 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

Slide 12

Slide 12 text

2 進数で表現できない 10 進数小数がある 10 進数小数には、2 進数で表現できない値がある 2. 小数と計算誤差 12

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

小数の 10 進数から 2 進数への変換方法 小数部が0 になるまで、下記を繰り返す 1. 小数部を2 倍する 2. 1 の結果の整数部の数字をその桁の数とする 2. 小数と計算誤差 16

Slide 17

Slide 17 text

2 進数で表現できる値 最終的に小数部が0 になる 10 進数 0.625 -> 2 進数 0.101 2. 小数と計算誤差 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

小数の表現方法には、二種類ある 固定小数点数 数値をビット列で表現する際に、あらかじめて整数部 と小数部が決まっている イメージ: 1.234 = 整数部 1 + 小数部 0.234 処理が高速 浮動小数点数 数値をビット列の指数表記で表現する イメージ: 0.01234 = 1.234 * 10^-2 表現できる数の幅が多い プログラムでは一般的 2. 小数と計算誤差 19

Slide 20

Slide 20 text

固定小数点数の構造 ビット列の中の小数点の位置が決まっている 表現できる整数、小数の桁数が決まっている int は、小数部を0 にした固定小数点数といえる 処理が早い 2. 小数と計算誤差 20

Slide 21

Slide 21 text

浮動小数点数の構造 ビット列を、 符号部 + 指数部 + 仮数部 にわける 仮数部: 実際の数値を、整数1 桁から始まる数値にず らしたもの ex) 0.123, 123 -> 仮数1.23 指数部: 仮数を何桁ずらすと実際の数値になるか 2. 小数と計算誤差 21

Slide 22

Slide 22 text

プログラムの小数では、浮動小数点の方が 使われることが多い 表現できる幅が固定小数点数よりはるかに大きい Go の float32 , float64 も、32bit, 64bit の浮動小数点数 2 進数にしたときに、浮動小数点数の仮数部がはみ出す 数字で計算誤差が起きる 2. 小数と計算誤差 22

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

float32 と float64 の構造 float32 , float64 は、それぞれ、IEEE 754 の規格に準拠 した32bit, 64bit の浮動小数点数となっている = 123.625 3. Go の組み込みの小数の挙動 24

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

定数にすると挙動が変わる 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

Slide 27

Slide 27 text

定数は型を持たないで存在することができ る 型を持たないため、型の制約を受けない ( 厳密には、値に応じて、 default type が存在して、変 数への代入時等に使用される) 具体的な変数への代入等した際に、型を持った値に変 換される( そのため、代入時にoverflow したりする) func main() { const a = 111111111111111111111 var _ = a } $ go run main.go constant 111111111111111111111 overflows int 3. Go の組み込みの小数の挙動 27

Slide 28

Slide 28 text

型を持たない定数の数値は任意の精度で計 算してくれる ただのふつうの数として扱われる https://go.dev/blog/constants 仕様上は精度の限界がない( 処理系に依存する) 3. Go の組み込みの小数の挙動 Numeric constants live in an arbitrary-precision numeric space; they are just regular numbers. “ “ 28

Slide 29

Slide 29 text

型なし定数は大きい数の定義に使える 例えば、円周率を高めの精度で定義しておける Pi = 3.14159265358979323846264338327950288419716939937510582097494459 https://pkg.go.dev/math#pkg-constants 3. Go の組み込みの小数の挙動 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Go で小数計算を行う際にはいくつかのアプ ローチがある 1. 整数に直す(Basis Point) 2. math/big Float 3. math/big Rat 4. shopspring/decimal 4. Go の小数計算のアプローチ 31

Slide 32

Slide 32 text

整数に変換することで、小数計算の問題を 回避することができる 元の数値を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

Slide 33

Slide 33 text

ユースケース次第では整数に変換するのが わかりやすい 桁数が限られている場合に有用 特に1 万分の1 を単位とするものを、万分率(Basis Point) という 万分率用の外部パッケージもある https://github.com/mercari/go-bps ただし、指定の桁以下の値の扱い( 切り上げ、切り捨 て、四捨五入) に注意する必要がある 4. Go の小数計算のアプローチ - 1. 整数に直す(Basis Point) 33

Slide 34

Slide 34 text

整数に直す方法が適さない場合もある 桁数が多かったり、可変の場合など 仮想通貨、為替変換...etc パッケージを利用しない限りは、自前で実装しないと いけないので、面倒ではある 4. Go の小数計算のアプローチ - 1. 整数に直す 34

Slide 35

Slide 35 text

math/big を使うとより高い精度で数を扱え る 以下の三つの type が用意されている Int: 多倍長整数 Float: 多倍長浮動小数点数 Rat: 有理数( 分数) 4. Go の小数計算のアプローチ - math/big 35

Slide 36

Slide 36 text

精度を一定上げられれば良いなら、 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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

分数で扱えば計算誤差もない 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

Slide 39

Slide 39 text

ただし big.Rat にも注意点がある 1. 小数で表現しきれない分数の取り扱い 2. 関数の操作が独特 4. Go の小数計算のアプローチ - 3. math/big Rat 39

Slide 40

Slide 40 text

小数で表現しきれない分数の取り扱いに注 意が必要 例) 小数で割り切れない分数で計算すると端数が消滅する 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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

big.Rat は単純な割り算・掛け算の計算に限 定した方が良さそう 単純な手数料や税金の計算などでは、使いやすく問題 ない 足し算・引き算や分配の計算では、小数に直す際に誤 差が起きうるので、注意が必要 4. Go の小数計算のアプローチ - 3. math/big Rat 42

Slide 43

Slide 43 text

shopspring/decimal でより安全に小数を扱 う 最大で2 の31 乗桁まで小数を誤差なく扱える 内部では、int(big.Int) の指数表記で持っている( つま り、整数の指数表記に直している) math/big のような独特なAPI をしていない 組み込みの型やJSON, XML などとの相互変換の関数も 提供されている https://github.com/shopspring/decimal 4. Go の小数計算のアプローチ - 3.shopspring/decimal 43

Slide 44

Slide 44 text

計算誤差も出ない 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

Slide 45

Slide 45 text

内部を全部 decimal ・外部を全部 string にす るアプローチ 小数を含みうる値をGo のコードではすべてdecimal で扱 い、API のIO やDB ではすべてstring にする 安全かつ平易で間違えづらい ただし、文字列変換・エラーハンドリングの手間とパ フォーマンスが許容する必要はある 4. Go の小数計算のアプローチ - 3.shopspring/decimal 45

Slide 46

Slide 46 text

パフォーマンスを比較してみた 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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

結果 : 整数に直す方法が一番早かった 早い順 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

Slide 49

Slide 49 text

4 章のまとめ 4. Go の小数計算のアプローチ 49

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

EOF