Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの数値クラス徹底解説:Integer・Float・BigDecimalの違いと内部の仕組み

Ruby 2.4では、それまで別々のクラスだったFixnumBignumが、Integerという単一のクラスに統合されました。そこで本記事では、Rubyにおけるさまざまな数値型について改めて整理してみたいと思います。

数値クラスの階層構造から、Floatの精度問題、BigDecimalとの使い分け、さらにはFixnumの内部的な仕組みまで、順を追って解説していきます。

数値クラスの全体像

まずは、Rubyの数値関連クラスの継承関係(クラス階層)を見てみましょう。

Numeric
  Integer
    Fixnum
    Bignum
  Float
  Complex
  Rational
  BigDecimal(標準ライブラリ)

ご覧のとおり、Numericクラスがすべての数値クラスの親クラスです。なお、ancestorsメソッドを使えば、任意のクラスの親クラスを簡単に調べられます。

例:

Fixnum.ancestors - Fixnum.included_modules

[Fixnum, Integer, Numeric, Object, BasicObject]

続いて、各クラスの特徴を表にまとめました。

クラス説明
IntegerFixnumBignumの親クラス1
FixnumOSの整数型(32bitまたは64bit)に収まる範囲の整数1
Bignumより大きな数値を扱うためのクラス111111111111
Float精度が保証されない小数5.0
Complex虚数を伴う数学的な計算に使用(1+0i)
Rational分数を正確に表現するために使用(2/3)
BigDecimal完全な精度を誇る十進小数3.0

Floatの精度問題とは?

Rubyの公式ドキュメントでは、Floatクラスは「不正確(imprecise)」であると明記されています。これは一体どういう意味なのでしょうか?

実際に試してみましょう。

0.2 + 0.1 == 0.3
# false

なぜfalseが返ってくるのでしょうか? 0.2 + 0.1の結果を見てみます。

0.30000000000000004

まさにこれこそが「不正確さ」の正体です。

この現象は、浮動小数点数がコンピュータ内部で2進数として保存される仕組み(IEEE 754規格)に起因しています。常に正確な小数計算が必要な場面では、BigDecimalクラスを活用しましょう。

FloatとBigDecimalの使い分け

BigDecimalは、任意精度の十進数を扱えるクラスです。

例:

require 'bigdecimal'

BigDecimal("0.2") + BigDecimal("0.1") == 0.3
# true

「では、なぜ常にBigDecimalを使わないのか?」——答えはシンプルで、処理速度が大きく劣るためです。

ベンチマーク結果はこちらです。

Calculating -------------------------------------
          bigdecimal    21.559k i/100ms
               float    79.336k i/100ms
-------------------------------------------------
          bigdecimal    311.721k (± 7.4%) i/s -      1.552M
               float      3.817M (±11.7%) i/s -     18.803M

Comparison:
               float:  3817207.2 i/s
          bigdecimal:   311721.2 i/s - 12.25x slower

ご覧のとおり、BigDecimalFloatの約12倍遅いため、デフォルトの数値型としては採用されていないのです。

FixnumとBignumの違い

ここからは、Ruby 2.4以前におけるFixnumBignumの違いを掘り下げてみましょう。

まずはコードから確認します。

1.class
# Fixnum

100000000000.class
# Bignum

Rubyは適切なクラスを自動的に選択してくれ、必要に応じてFixnumBignumへ自動的に昇格させます。

補足: 64ビット環境のRubyインタプリタでは、Bignumオブジェクトを得るために、さらに大きな数値が必要になる場合があります。

なぜ複数のクラスが必要だったのでしょうか? その理由は、巨大な数値を扱うには別の実装が必要で、しかもその処理は低速になるからです。つまり、FloatBigDecimalの関係とよく似た構図になっているのです。

Fixnumの特殊な性質:object_idの秘密

Fixnumクラスには、いくつかユニークな特徴があります。たとえば、object_idが特定の計算式によって決まるという点です。

1.object_id
# 3
20.object_id
# 41

その式は (number * 2) + 1 です。

さらに驚くべきことに、Fixnumを使うとき、実はオブジェクトは一切生成されていません。保存すべきデータを持たず、値そのものがオブジェクトIDから導出される仕組みになっているのです。

これはあくまで実装上の詳細ですが、知っておくとちょっとしたトリビアとして楽しめる話題です。

MRI(Matz's Ruby Interpreter)では、値とオブジェクトIDの相互変換に、次の2つのマクロが使われています。

INT2FIX(i)  ((VALUE)(((SIGNED_VALUE)(i))<<1 | FIXNUM_FLAG))
FIX2LONG(x) ((long)RSHIFT((SIGNED_VALUE)(x),1))

ここで行われているのは「ビットシフト」と呼ばれる操作で、すべてのビットを左または右にずらす処理です。

1ビット左にシフトすることは2倍することと等価であるため、式が (number * 2) + 1 になります。最後の +1 は FIXNUM_FLAG 由来のものです。

一方、Bignumは通常のクラスと同様に振る舞い、普通のオブジェクトIDを使用します。

111111111111111.object_id
# 23885808

つまり、インタプリタレベルで見ると、Fixnumオブジェクトはシンボルに近い性質を持ち、Bignumオブジェクトは文字列に近い性質を持つということです。

Ruby 2.4以降のIntegerの扱い

Ruby 2.4以降、FixnumとBignumは非推奨(deprecated)となりましたが、内部的な動作は従来どおりです。

Rubyは自動的に両者を切り替えます。しかも、クラス名を意識する必要は一切ありません。

そのため、小さなIntegerの数値は、これまでのFixnumとまったく同じように動作し続けます。

まとめ

本記事では、Rubyに存在するさまざまな数値関連クラスについて学びました。

  • Floatは不正確なため、精度が性能よりも重要なケースではBigDecimalを使うのが有効
  • Fixnumオブジェクトはインタプリタレベルで特殊な扱いを受けるが、Bignumはごく普通のオブジェクト

この記事が参考になった方は、ぜひ下部のフォームからニュースレターにもご登録ください。

  1. Ruby 2.6のMJITとは?仕組みとパフォーマンスを徹底解説

    Rubyのパフォーマンスは、バージョンが上がるごとに大きく向上してきました。そしてRuby開発チームは、さらに速いRubyを実現するために全力を尽くしています。 その取り組みのひとつが「3×3プロジェクト」です。 その目標とは? Ruby 3.0を、Ruby 2.0の3倍速くすることです。 このプロジェクトの一環として誕生したのが、新しいMJITコンパイラです。これが本記事のテーマとなります。 MJITとは何か MJITは「Method Based Just-in-Time Compiler(メソッドベースのジャストインタイムコンパイラ)」の略称です。 これは具体的にどういう意味でしょうか?

  2. Rubyの配列(Array)クラスの使い方徹底解説!基本操作と便利なメソッド一覧

    配列(Array)とは? 配列はRubyに組み込まれたクラスで、0個以上の要素をリスト形式で保持することができます。さらに、要素の追加・アクセス・繰り返し処理を簡単に行えるメソッドも多数用意されています。 もし配列が存在しなければ、値ごとに個別の変数を用意しなければならず、管理が非常に大変になります。 例: a = 1 b = 2 c = 3 しかし、配列を使えば次のように書けます: numbers = [1, 2, 3] さらに嬉しいことに、配列の中には何でも入れられます! 例えば: 数値 文字列 さらに別の配列!(これは多次元配列になります) それでは、配列を最大限に活用できる