「Ruby のしくみ」という本

VMベースのインタプリタ型言語処理系であるRubyがコードをどのように解釈し、どうやって実行するか、そのしくみを解説。Rubyについての基礎知識がなくても、図版と短いコードの実験を多用した構成により、そのしくみについて理解することができます。 実務でRubyは使...

www.ohmsha.co.jp

尚, この本の詳しい紹介は「るびま」の以下の記事に書かれている.

この本を購入した当初, パラパラとめくって, 「ポインタ」とか「構造体」とか, Ruby を使う上であまり気にする必要の無さそうな言葉が随所に出てきたのを見て, 「俺には百万年早い本を購入してしまった…」と思ってしまったが, クラスの継承やメソッド探索, モジュールの mix-in がどんな仕組みで行われているのか, ちゃんと理解したいと思うようになり, 改めてこの本を手にとって読んでみることにした.

前提

自分の Ruby レベル

  • Ruby 初心者検定 (そんなの無いけど) Hello world 2 級程度, ざっくり言うと, 初心者を卒業できない留年生
  • ギョームで, ちょっとしたツールを書く程度
  • Ruby 技術者認定試験 Gold を 2 回受験するも, 62 点, 70 点と惨敗, 死ぬまでには取得したいと思っている

自分の C レベル

この本を読み進める上で, C 言語の知識が必要だと思うが…

  • Hello world 8 級で, ポインタや構造体という言葉はまだまだ, ちゃんと理解出来ていない
  • もちろん, ギョームで C を使うことはない

というレベルの人間が読んで, それをメモるので誤記や誤認が多々あると思うが, その際は気付いた時点で修正する予定.

メモ

冒頭

5 章の冒頭に書かれていて, いきなりスッと入ってきたので引用させて頂く.

すべての値はオブジェクトであり、すべての Ruby プログラムはオブジェクトの集合と、それらの間のメッセージ送信から成り立つ。

Ruby オブジェクトの内側

RObject と RBasic という構造体

  • Ruby はクラスオブジェクトを, RObject という C 構造体に保存する

出たな, 構造体…

ここで, 構造体について詳しく触れると辛いので構造体は箱として考えるようにして読み進めることにした.

RObject 構造体は以下のような構成となっている.

  • VALUE ポインタ (Ruby は何らかの値の参照に VALUE ポインタを用いる) に指示されている
  • RBasic 構造体と独自に作成したクラスのオブジェクトに固有の情報を含んでいる
  • numiv にはインスタンス変数の数が格納されている
  • ivptr にはインスタンス変数の配列へのポインタが格納されている

さらに出たな…, ポインタ. メモリのアドレスを記録しておく為の変数という理解で読み進めたいと思う.

RBasic 構造体は以下のような情報を含んでいる.

  • 真偽値集合 flags
  • オブジェクトが何のインスタンスであるかを示すクラスポインタが klass に格納されている

klass と ivptr

Ruby が RObject をどのように使うのか理解する為, 簡単なクラスからインスタンスを作って確認しながら読み進める.

class Orenoclass
attr_accessor :foo
attr_accessor :bar
end

ちなみに, ここで利用する Ruby のバージョンは以下の通り.

$ ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

以下のようにインスタンスを生成すると, RObject 内にそのクラスへのポインタを保存する.

irb(main):001:0> class Orenoclass
irb(main):002:1> attr_accessor :foo
irb(main):003:1> attr_accessor :bar
irb(main):004:1> end
=> nil
irb(main):005:0> ore = Orenoclass.new
=> #<Orenoclass:0x000055c0873c1110>
  • #<Orenoclass: で ore オブジェクトが持つクラスポインタの値を表示している
  • 16 進数 0x000055c0873c1110 の値はオブジェクトへの VALUE ポインタを示している
  • この値はインスタンス毎に値が異なる
irb(main):006:0> ore.foo = 'Foo'
=> "Foo"
irb(main):007:0> ore.bar = 'Bar'
=> "Bar"
irb(main):008:0> ore
=> #<Orenoclass:0x000055c0873c1110 @foo="Foo", @bar="Bar">
  • 上記のように Ruby はオブジェクト内に保存された値を追跡する為に, インスタンス変数の配列を使用する
  • オブジェクト毎に異なる値を持つことが出来るので, インスタンス変数の配列をオブジェクト毎に持つ必要がある

クラスから生成したインスタンスの可視化

以下のように Orenoclass から ore と washi という 2 つのオブジェクトを生成する.

# Rclass Orenoclass
class Orenoclass
attr_accessor :foo
attr_accessor :bar
end

# RObject ore
ore = Orenoclass.new
ore.foo = 'Foo'
ore.bar = 'Bar'

# RObject washi
washi = Orenoclass.new
washi.foo = 'Baz'

これによって, RClass 構造体と 2 つの RObject 構造体を生成することになる.

  • 各 RObject 構造体の klass の値は Rclass 構造体へのポインタとなっている
  • 各 RObject 構造体は別々のインスタンス変数の配列を持っている

以下の記述について, 2 回くらい読みなおしたけど読み解けなかったので, そのまま引用する.

どちらの配列も RObject 構造体への参照に Ruby が使用するのと同じポインタ、 VALUE ポインタを含んでいる

ユーザー定義のクラスオブジェクトと一般的なオブジェクト

  • ユーザーが定義したクラスのオブジェクトは RObject 構造体に保存する, また, Ruby が内部的に作成する幾つかのクラスのインスタンスも保存する
  • ユーザー定義クラスのオブジェクトに対して, 整数や文字列, シンボル等は一般的なタイプとして参照する
  • 文字列の値は RString 構造体, 配列は RArray 構造体, 正規表現は RRegexp 構造体に保存する
  • これらの構造体において, RBasic 情報を共有している

構造体を必要としない場合

  • Ruby は小さな整数値, シンボルのような単純な値は構造体を使わない
  • 構造体を使わず VALUE ポインタの中に直接保存する
  • 単純なデータ型には, クラスポインタは存在しない代わりに, VALUE の下位数ビットに保存したフラグを使ってクラスを記録する
irb(main):010:0> "string".class
=> String
irb(main):011:0> 1.class
=> Integer
irb(main):012:0> :symbol.class
=> Symbol

上記のように, 整数値や文字列等の一般的な値は全てオブジェクトであることが解る.

「一般的」なオブジェクトはインスタンス変数を持てるのか

「一般的」オブジェクトにもインスタンス変数があるのか…整数, 文字列もオブジェクトであるならば, インスタンス変数があるということになるが…「一般的」オブジェクトは RObject 構造体を使わないので, そのインスタンス変数はどこに保存するのか.

irb(main):001:0> str = "foo bar baz"
=> "foo bar baz"
irb(main):002:0> str.instance_variables
=> []
irb(main):003:0> str.instance_variable_set('@val1', 'val1')
=> "val1"
irb(main):004:0> str.instance_variables
=> [:@val1]
irb(main):005:0> str.instance_variable_set('@val2', 'val2')
=> "val2"
irb(main):006:0> str.instance_variables
=> [:@val1, :@val2]
irb(main):007:0> str.class
=> String

おお, String クラスのオブジェクトに対して, インスタンス変数を保存出来ている. (※ Ruby 2.0 から, 数値クラス, Ruby 2.1 から Synbol オブジェクトが freeze されるようになり, インスタンス変数を持てなくなっている)

で, 「一般的」なオブジェクトのインスタンス変数はどこに保存するのか

  • RObject を使用しないオブジェクトのインスタンス変数を保存する為, generic_iv_tbl という特殊なハッシュに値を保存する

インスタンス変数を保存する時間

以下のコードを使って, テスト用のオブジェクトを生成するベンチマークを実施する.

require 'benchmark'

ITERATIONS = 100000
GC.disable
obj = ITERATIONS.times.map { Class.new.new }
Benchmark.bm do |bench|
  20.times do |count|
    bench.report("#{count+1}") do
      ITERATIONS.times do |n|
        obj[n].instance_variable_set("@var#{count}", "value")
      end
    end
  end
end

書籍内の結果では, 新しく生成したオブジェクトに対して, 20 回新しいインスタンス変数を追加する処理の結果として, 以下のように書かれている.

  • 20 回のうち, 途中でインスタンス変数を追加する為の時間が長くかかることがある
  • これは RObject 構造体でインスタンス変数を格納する ivptr の振る舞いに起因する

尚, Ruby 1.8 と Ruby 1.9 以降で以下のように挙動が異なる.

  • Ruby 1.8 では ivptr は変数名と値の両方を含むハッシュテーブルで, 自動で拡張されるようになっていた
  • Ruby 1.9 以降では, インスタンス変数を処理する為に大きな配列を予め確保するか, インスタンス変数を増やす度に配列のサイズを繰り返し増やしていく必要がある

Ruby 1.9 以降では, 配列のサイズを増やすタイミングで処理時間が長くなってしまうが, その後の処理で配列のメモリを確保し直す必要がないということらしい.

ということで, 上記のベンチを手元の環境でも実行してみたら, 以下のような結果となった.

$ ruby --version
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]
$ ruby b.rb 
       user     system      total        real
1  0.076000   0.000000   0.076000 (  0.076277)
2  0.060000   0.000000   0.060000 (  0.059735)
3  0.056000   0.000000   0.056000 (  0.057135)
4  0.084000   0.000000   0.084000 (  0.081542)
5  0.072000   0.012000   0.084000 (  0.086883)
6  0.088000   0.000000   0.088000 (  0.088172)
7  0.056000   0.008000   0.064000 (  0.064494)
8  0.064000   0.008000   0.072000 (  0.071839)
9  0.100000   0.000000   0.100000 (  0.098734)
10  0.060000   0.004000   0.064000 (  0.061413)
11  0.068000   0.004000   0.072000 (  0.072650)
12  0.052000   0.008000   0.060000 (  0.062495)
13  0.056000   0.008000   0.064000 (  0.062703)
14  0.080000   0.000000   0.080000 (  0.081709)
15  0.056000   0.004000   0.060000 (  0.063758)
16  0.060000   0.004000   0.064000 (  0.062303)
17  0.152000   0.008000   0.160000 (  0.158529)
18  0.108000   0.008000   0.116000 (  0.115108)
19  0.080000   0.000000   0.080000 (  0.081490)
20  0.060000   0.012000   0.072000 (  0.072019)

9 回目と 17 回目だけ, 極端に処理時間が長いことが解る.

ここまでのまとめ

  • Ruby のクラスは RObject という構造体に保存する
  • RObject 構造体には RBasic 構造体と独自に作成したクラスのオブジェクトに固有の情報を含んでいる
  • 但し, 文字列や数値, シンボル等のクラスのオブジェクトについては, RObject 構造体は利用せず, それぞれの構造体に保存する
  • RObject を使用しないオブジェクトのインスタンス変数を保存する為, generic_iv_tbl という特殊なハッシュに値を保存する
  • Ruby 1.9 以降では, メモリ削減の為, 単純な配列に保存するよになった
  • インスタンス変数を保存する為の ivptr 配列は, インスタンス変数を増やす度に配列のサイズを繰り返し増やしていく
  • 配列のサイズを増やす際に, メモリ確保の為に処理時間が長くなる

以上

メモでした. 後編に続く…はず.

元記事はこちら

Ruby のしくみ ~ Ruby Under a Microscope ~ 読書メモ ~ 5 オブジェクトとクラス (前編) ~