こんにちは、hachi8833です。
BigBinaryシリーズ、今回はあえて2008年のRuby記事を元に検証してみました。当時と状況がいろいろと変わっていることがわかりました。この記事は元記事の要点の翻訳と、それについての考察の2つで構成されています。
RailsコンソールでActiveRecordモデルを適当に#new.methods
すると既存のメソッド一覧を取れますが、昔のRailsではその中の#id
がRuby 1.xの#id
(現在の#object_id
に相当)とかぶっていたのでwhiny_nils
オプションで対応していたことを知りました。
普段気にすることもないnil
オブジェクト内部のobject_id
の値がむき出しで返ってきたらびっくりしますね。詳しくは本文をお読みください。
元記事
- Why the id of nil is 4 in Ruby(米国BigBinary社のブログより)
- 参考: HackerNewsの過去記事のコメント: Ask Matz: why the id of nil is 4? (bigbinary.com)
元記事の逐次翻訳ではありませんのでご了承ください。本記事タイトルも検証内容を踏まえて元記事から変更いたしました。
別記事について
元記事を下敷きにした本記事とは別に、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...)に、以下のようにfalse
、true
、nil
が割り当てられました。
false
のオブジェクトIDは0、true
は2、nil
は4です。
Ruby 1.9(当時)がリリースされればRubyのid
はobject_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, thenUSE_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)になったということのようです。
nil
、true
、false
といった特殊定数の値を通常のRubyコーディングで直接利用することはまずないので普段は気にする必要はないと思いますが、定数変更の背景を知ることができてすっきりしました。
関連記事(BigBinaryシリーズ)
- Rails4で
whiny_nils
オプションが廃止された理由 - [Rails 5] rakeタスクがrailsコマンドでもできるようになった
- [Rails 5] Rails 5の新フレームワークデフォルト設定ファイルでアップグレード作業を軽減する
- [Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する