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. Floating Point Numbers and
    Decimal in Go
    Ryoya Sekino

    @Go Conference 2021 Autumn

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 浮動小数点数の構造
    ビット列を、 符号部 +
    指数部 +
    仮数部 にわける
    仮数部:
    実際の数値を、整数1
    桁から始まる数値にず
    らしたもの

    ex) 0.123, 123 ->
    仮数1.23
    指数部:
    仮数を何桁ずらすと実際の数値になるか
    2.
    小数と計算誤差
    21

    View Slide

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

    View Slide

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

    View Slide

  24. float32

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

    View Slide

  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

    View Slide

  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

    View Slide

  27. 定数は型を持たないで存在することができ

    型を持たないため、型の制約を受けない

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

    View Slide

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


    28

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  33. ユースケース次第では整数に変換するのが
    わかりやすい
    桁数が限られている場合に有用
    特に1
    万分の1
    を単位とするものを、万分率(Basis
    Point)
    という
    万分率用の外部パッケージもある

    https://github.com/mercari/go-bps
    ただし、指定の桁以下の値の扱い(
    切り上げ、切り捨
    て、四捨五入)
    に注意する必要がある
    4. Go
    の小数計算のアプローチ - 1.
    整数に直す(Basis Point)
    33

    View Slide

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

    View Slide

  35. math/big
    を使うとより高い精度で数を扱え

    以下の三つの
    type
    が用意されている
    Int:
    多倍長整数
    Float:
    多倍長浮動小数点数
    Rat:
    有理数(
    分数)
    4. Go
    の小数計算のアプローチ - math/big
    35

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  43. shopspring/decimal
    でより安全に小数を扱

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  53. EOF

    View Slide