使っているライブラリを見てみよう(shopspring/decimal編)

こんにちは. Poolでエンジニアをしている @always_allokay です。

こちらはカンム Advent Calendar 2024 の6日目の記事です。 昨日はprinさんによるボボステの記事でした。

タイトルの通り、お世話になっているライブラリをちょっと見てみるシリーズです。(今はじめました)
今回は、shopspring/decimal について見ていきます。

shopspring/decimalとはなにか

まずはライブラリのREADMEをみます。

Arbitrary-precision fixed-point decimal numbers in go.

Note: Decimal library can "only" represent numbers with a maximum of 231 digits after the decimal point.

記載の通り、「Goでの任意精度固定小数点10進数」を実現するライブラリです。

浮動小数点はよく聞くワードですが、固定小数点とはなにか?という疑問が湧きます。 詳細は以下の資料にゆずりますが、数値を格納するときに、整数部と小数部の桁数をあらかじめ決めてある、というのが固定小数点です。 とてもわかりやすくて良いスライドなのでぜひ一読をお勧めします。

speakerdeck.com

なにを解決するライブラリか?

さきほどのスライドでも言及がありますが、コンピュータでは最終的に2進数でデータを扱う都合上、小数点以下を厳密に表現することができない、というよく知られた問題があります。
日常的にプログラミングする分には問題ない精度であることがほとんどなのですが、金融や決済などの領域では、致命的な誤差になりえます。 これを解決(≒ほとんどの場合に問題ないように)するためのライブラリです。
カンムでも、決済や利率計算の際に誤差がでないように利用しています。

ライブラリの構造

それではさっそくみていきます。まずはrepository全体を概観。

$ tree .
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── const.go
├── const_test.go
├── decimal-go.go
├── decimal.go
├── decimal_bench_test.go
├── decimal_test.go
├── go.mod
└── rounding.go

1 directory, 11 files
$ wc -l *
      76 CHANGELOG.md
      45 LICENSE
     139 README.md
      63 const.go
      34 const_test.go
     415 decimal-go.go
    2339 decimal.go
     314 decimal_bench_test.go
    3649 decimal_test.go
       3 go.mod
     160 rounding.go
    7237 total

全体でもたった11ファイル、テストコードを除けば、3000行弱のソースコードで、実現されていることがわかります。

github.com

// Decimal represents a fixed-point decimal. It is immutable.
// number = value * 10 ^ exp
type Decimal struct {
    value *big.Int

    // NOTE(vadim): this must be an int32, because we cast it to float64 during
    // calculations. If exp is 64 bit, we might lose precision.
    // If we cared about being able to represent every possible decimal, we
    // could make exp a *big.Int but it would hurt performance and numbers
    // like that are unrealistic.
    exp int32
}

ライブラリの基礎となるdecimal.Decimal structに注目すると、Decimalは、*big.Int型のvalueとint32型のexpの二つからなることがわかりました。 そして、このライブラリが提供する最大精度もexpがint32であることに起因することがわかります。 であれば、これをより大きな数値を扱える型にするとよいのではと安直に思ってしまいますが、それに関する2つのトレードオフについてコメントされています。

  • ひとつは、64bitにする場合について。計算途中で、float64にキャストする部分があり、そこで桁溢れを起こさないためとありますね。 オーバーフローを起こすと、精度云々ではなく数値がぶっ壊れてしまうのでこれは納得。
    • 実際にfloat64にcastしている箇所は以下でした。 github.com
  • もうひとつは、big.Intなどとする場合について。こちらは性能が損なわれてしまうことと、そこまで大きな数値を扱うケースが現実的ではないことから使わないと判断されているようです。

一つ賢くなりました。

さらに、Add, Sub, Mul, Divなどの四則演算をみていくと、実はMulメソッドは、不正確な数値を返しそうになる場合panicを発生させていることを発見しました。 普段使ってるメソッドにも知らないことがあるものですね。とはいえ、現実に扱う数値では遭遇することはなさそうです。

github.com

そのほかの発見としては、丸め処理にも、通常の四捨五入以外にも、BankerRouding、CashRouding, RoundDown, RoundUpという丸め処理があることがわかりました。
それぞれ、近い方の偶数に丸める、最小通貨単位に丸める、0に近づくように丸める、0から遠ざかるように丸めるという処理です。
現在は利用していないのですが、CashRoundingは実は任意の円単位(千円、万円、億円 etc)に数字を丸めるときに使えるかもしれません。

得られた知見

  • shopspring/decimalは、231桁の精度まで担保している。これは言い換えると小数点以下2,147,483,647桁≒小数点以下21億桁の精度なので、現実的には十分な精度である。
  • 丸め処理にはBankerRouding, CashRoudingをはじめ多くのバリエーションがあること。

一言

もうちょい深掘りしたかったのですが、アルゴリズムとか最適化らへんは歯が立たなかったです。大学生ぶりにアークタンジェントとかみました。 つぎは標準ライブラリや準標準(x/exp/...)のライブラリを読みたいですね。

カンムはソフトウェアエンジニアを募集しています

team.kanmu.co.jp