はじめに
これまで、リアルなお金を扱うシステムを作ったこともなかったし、小数を含む演算を実装することがこれまでなかったので小数演算の経験がほとんどありませんでした。(ほんと整数だけで大抵のことは大丈夫!!!)
Float
を使うと まるめ誤差 が生じて四捨五入や繰り上げ、繰り下げを利用するときに誤差が影響する場合があるということは話に聞いていたので、実際にどういう影響があるのか、対応する方法などについてRubyを使って調べてみました。
そもそも小数とは
まずは小数についてをざっくり調べて見ると、、 浮動小数点数 - Wikipedia とか 固定小数点数 - Wikipedia とか色々出てくる。。これらをざっくり読むだけではうーんという感じなのでとりあえず表記方法だけみていこうと思います。
小数の表記方法
小数といえば、例えば 1.23
これです!! 小数点の表記方法としては他には以下のような書き方ができます。全て 1.23
という小数を表しています。
- 1.23 X 100
- 0.123 X 101
- 123 X 10-2
仮数部 X 基数 指数部 というような構造となっています。
さらにその他でよく見るのは基数となる 10
を固定して以下のような表記です。
- 1.23E0
- 0.123E1 = 0.123E+1
- 123E-2
仮数部E指数部 というような構造となっています。(指数部には正の値の場合は +
をつけても付けなくても同じです。)
上記のように 仮数
、 基数
、 指数
で表現される小数は浮動小数点の表記方法となります。
参考
Float と BigDecimal
Floatについて
-
class Float (Ruby 2.4.0) によるとRuby の Float は 浮動小数点数のクラス で 実装は C言語 の double ということみたいです。
-
以下のように小数点を含んだ数値をリテラルで表現すると
Float
として扱われます。- プログラム上意識せずリテラルをつかて小数を扱っている場合は Float として扱っていたということになります。
1.2.class
> Float
Floatによる誤差について
irbなどで実行してみるとわかると思います。
1.2 - 1.0
> 0.19999999999999996 #<= 0.2 を期待するがズレが生じる
10.0 / 6.0
=> 1.6666666666666667 #<= 1.66666...と無限に続くはずがまるめられる
IEEE754 規格による2進数による浮動小数点計算では対応できない値の結果がまるめられ誤差が生じるということらしいです。詳しくは以下の記事などがとても参考になります。
- 浮動小数点数について本気出して考えてみた - 一から勉強させてください( ̄ω ̄;)
- 【Java初心者】いざとなったらよくわからなかったので改めて調べた~floatとdoubleに誤差が出る理由~ - Qiita
- IEEE 754 - Wikipedia
BigDecimal について
- library bigdecimal (Ruby 2.4.0) によると、 浮動小数点数演算ライブラリであり、任意の精度で10進数で表現された浮動小数点を扱えます。
-
<浮動小数点数> = 0.xxxxxxxxx*10**n
という 10進形式で数値を保持します。 - なぜBigDecimalを使わなければならないのか | Java好き の記事にもあるように2進数の浮動小数点演算で生じる丸め誤差が発生せず演算ができるライブラリです。
- Floatのように組み込みライブラリではないので以下のように
require
する必要があります。
require 'bigdecimal'
require 'bigdecimal/util' #<= to_d メソッドが使えるようになる
puts ('1.2'.to_d - 1.0) == 0.2
> true #<= BigDecimalの演算ではtrueになる
puts (1.2 - 1.0) == 0.2
> false #<= Floatの演算ではfalse
※ BigDecimalをインスタンス化する際の注意
↑ 小数の値を 文字列 で渡して '1.2'.to_d
しているのには理由があります。
小数点のリテラル表記の場合は Float として扱われるのでFloatで表現できる桁数で丸められてから to_d
されてしまうからです。以下が実行例です。
puts '1.111111111111111111222222222'.to_d
> 0.1111111111111111111222222222E1 #<= 想定通りの桁数となる
puts 1.111111111111111111222222222.to_d
> 0.111111111111111E1 #<= 切り捨てられた桁がある
FloatとBigDecimalのパフォーマンスについて
float(double) を利用するメリットは丸め誤差が生じるけれど、それを無視できるのであればパフォーマンスがいいということです。アニメーションの演算などそういうのは多少の誤差は許容できるのでfloatが使われ、きちんとBigDecimalを使った演算をするときはお金の計算など少しの誤差も許容できない場合です。
その中でパフォーマンスについて簡単なスクリプトで実行してみた所、たしかにBigDecimalのほうが若干遅い感じはしました。(そんなに差はないかも)
require 'benchmark'
require 'bigdecimal'
require 'bigdecimal/util'
n = 100_000
Benchmark.bm(10) do |b|
b.report('float') { n.times{ ('1.2'.to_f - 1.0).to_s } }
b.report("bigdecimal") { n.times{ ('1.2'.to_d - 1.0).to_s } }
end
> user system total real
> float 0.120000 0.000000 0.120000 ( 0.125545)
> bigdecimal 0.530000 0.010000 0.540000 ( 0.528924) #<=若干遅い
丸め処理の指定を切り替えることが出来る
@rryu さんにコメントで指摘いただいたので修正します。
BigDecimalといえど、丸め誤差は生じます。 10進数の演算で 10/3
などは 3.33333...
と無限に続くからです。irbなどで見てみると以下のようになります。
puts 10.0.to_d/3
> 0.3333333333333333333E1
BigDecimalを使うことでその丸め処理を任意に変更することができます。デフォルトでは四捨五入となっているようですが、modeを変えることで切替えられるます。
puts '0.1254'.to_d.round(3)
> 0.125E0 #<= 4が四捨五入で切り捨てられている
# 全てを切り上げるように設定する
BigDecimal::mode(BigDecimal::ROUND_MODE, BigDecimal::ROUND_UP)
puts '0.1254'.to_d.round(3)
> 0.126E0 #<= 4が切り上げられている
以下の記事がとても参考になりました
- なぜBigDecimalを使わなければならないのか | Java好き
- 小数点の計算をやるからといってすぐにfloatやdoubleを使ってはいけない - じゅんいち☆かとうの技術日誌
- [Ruby]消費税計算にはBigDecimalを使いましょう - Qiita
以上
ということでざっくり小数について調べてみたことを書いてみました。