Ruby2.0でnil.object_idの値が4から8に変わった理由

こんにちは、hachi8833です。

BigBinaryシリーズ、今回はあえて2008年のRuby記事を元に検証してみました。当時と状況がいろいろと変わっていることがわかりました。この記事は元記事の要点の翻訳と、それについての考察の2つで構成されています。

RailsコンソールでActiveRecordモデルを適当に#new.methodsすると既存のメソッド一覧を取れますが、昔のRailsではその中の#idがRuby 1.xの#id(現在の#object_idに相当)とかぶっていたのでwhiny_nilsオプションで対応していたことを知りました。

普段気にすることもないnilオブジェクト内部のobject_idの値がむき出しで返ってきたらびっくりしますね。詳しくは本文をお読みください。

元記事

元記事の逐次翻訳ではありませんのでご了承ください。本記事タイトルも検証内容を踏まえて元記事から変更いたしました。

別記事について

元記事を下敷きにした本記事とは別に、whiny_nilsについて検証しました。

1. RubyのnilのオブジェクトIDが4である理由(元記事より)

環境: Ruby 1.8.7とRails 2.3

Rails開発中に以下のようなメッセージが表示されることがよくあります。

Called id for nil, which would mistakenly be 4 — if you really wanted the id of nil, use object_id

ご存知のとおり、このメッセージはRailsによって追加されたものであり、「whiny nil」と呼ばれるものです。Rails(当時)のconfig/development.rbファイルを開くと次の設定があります。

# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true

この設定がオンになっていると、アプリケーションのコードでnilに対してidメソッドを呼んだときにエラーを表示します。この動作は望ましくないため、Railsではこの場合に例外をスローします。

ところでなぜ「4」なのでしょう。matzがなぜnilをこの値に割り当てた理由が気になりました。

その答えをRuby Conference 2008の動画で見つけました。

要するにmatzはRuby内部で数値オブジェクトのIDを奇数に割り当てたのです。

訳注: 以下の画像はRuby 2.4での確認ですが、以前のRubyでも同様です。

そうやって空いた偶数(0、2、4…)に、以下のようにfalsetruenilが割り当てられました。

falseのオブジェクトIDは0、trueは2、nilは4です。

Ruby 1.9(当時)がリリースされればRubyのidobject_idに変更されるので今後は問題にならないと考えられます。ここではこれ以上は追いませんので、詳しくはHackerNewsのコメントをご覧ください。

2. object_idの値が2.0で4から8に変わった理由

元記事の要点は以上となります。ここから、Rubyのobject_idの値についてさらに追ってみました。

確認に使った環境

  • Rubyバージョン: 1.8〜2.4.0p0

Ruby 2.0でnilオブジェクトのIDが8に変更された

BigBinaryの元記事を検証していて、nilのオブジェクトIDの値がRubyのバージョンによって違っていることに気が付きました。

1.8/1.9では元記事どおり4ですが、2.0以降はnil.object_idの値が8になっています。以下は2.4の場合です。

object_idの値はどこで定義されているか

元記事執筆当時(Ruby 1.8.7)のIDはFixNumでした。FixNumの範囲でさまざまなオブジェクトIDをある程度効率よく割り当てられるよう、数値オブジェクトには奇数を割り当て、数値以外のオブジェクトは偶数に割り当てたと推測しました。

実装については前述のHackerNewsのコメントで詳しく議論されています。

コメントをチェックしたところ、どうやら「数値を上位桁に1回ビットシフトして1を足す」という軽い演算(例: 数値の7は(7 << 1) + 1 # => 15となる)で数値を奇数のオブジェクトIDに変換しているとのことです。「C言語らしいやり方かもね」というコメントもあります。

元記事でも述べられているように、偶数の最初の範囲を特殊定数用に予約し、falseに0、trueに2、nilに4をそれぞれ割り当てていました。

当時の特殊定数がどこで定義されているのかを調べてみたところ、Ruby 1.9.3のinclude/ruby/ruby.hで見つけたので以下に貼ります。

/* special constants - i.e. non-zero and non-fixnum constants */
enum ruby_special_consts {
    RUBY_Qfalse = 0,
    RUBY_Qtrue  = 2,
    RUBY_Qnil   = 4,
    RUBY_Qundef = 6,

    /*略*/
};

RUBY_Qnil定数がnilの定義ですね。

2.0での変更点

今度はRuby2.0での特殊定数の定義を調べてみました。

1.xにはなかったUSE_FLONUMが条件に追加されています(以下は2.4のinclude/ruby/ruby.hより)。

#if USE_FLONUM
    RUBY_Qfalse = 0x00,     /* ...0000 0000 */
    RUBY_Qtrue  = 0x14,     /* ...0001 0100 */
    RUBY_Qnil   = 0x08,     /* ...0000 1000 */
    RUBY_Qundef = 0x34,     /* ...0011 0100 */
    /*略*/
#else
    RUBY_Qfalse = 0,        /* ...0000 0000 */
    RUBY_Qtrue  = 2,        /* ...0000 0010 */
    RUBY_Qnil   = 4,        /* ...0000 0100 */
    RUBY_Qundef = 6,        /* ...0000 0110 */
    /*略*/
#endif

USE_FLONUMの場合はfalseがゼロ、trueが0x14(10進数だと20)、nilが8にそれぞれ割り当てられています。

USE_FLONUMという名称から浮動小数点関連であることが想像できます。そしてRuby 2.0のChangeログでついに該当箇所を見つけました。

include/ruby/ruby.h
introduce flonum technique for 64bit CPU environment (sizeof(double) == sizeof(VALUE)). flonum technique enables to avoid double object creation if the double value d is in range about between 1.72723e-77 < |d| <= 1.15792e+77 or 0.0. flonum Float value is immediate and their lowest two bits are b10. If flonum is activated, then USE_FLONUM macro is 1. I’ll write detailed in this technique on bugs.ruby-lang.org/projects/ruby-trunk/wiki/Flonum_tech
Ruby 2.0のChangeログより一部を強調して引用

64ビット環境の浮動小数点オプションに対応した結果だったんですね。ということは、USE_FLONUMがオフになる環境では従来どおりnil.object_idは4になるはずです。

上のリリースノートにあるWikiへのリンク先は空になっていたので、さらに当時のチケット#6763を掘り当てました。ko1ことKoichi Sasadaさんのお仕事でした。

On our measurements, we can achieve x2 performance improvement for simple floating calculation.
#6763より

USE_FLONUMの導入は64ビット環境でのパフォーマンス向上が目的であり、ビットパターンの関係からnilのオブジェクトIDの値が0x08(2進数: 0000 1000)になったということのようです。

niltruefalseといった特殊定数の値を通常のRubyコーディングで直接利用することはまずないので普段は気にする必要はないと思いますが、定数変更の背景を知ることができてすっきりしました。

関連記事(BigBinaryシリーズ)

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ