K.Sasada's Home Page

Diary - 2012 December

研究日記

師走

_25(Tue)

ご連絡:本日 25 日まで続けられた Ruby VM アドベントカレンダーは,世界の終了のため,保存していなかった部分が消えてしまいました.今後,随時復活させていきたいと思います.ご迷惑をおかけ致します.


ご連絡:世界の終了によって失われた記憶を随時復旧させていますが,いくつかの記憶のかけらが宇宙的な何かのために欠落してしまっているようです.鋭意,そうさくしていきたいと思っております.


一覧:


というわけで, Ruby VM アドベントカレンダーの 25 日目,最終日です.

途中から Ruby 2.0.0 の紹介記事みたいになっていましたが,VM まわりの機能をなんとなく紹介出来たんじゃないかと思います.

さて,2.0 でやり残したことはあるかと言われると,それが沢山あるのです.ちょっと細かいことを解説していくと大変なので,判る人にしか判らないが,判る人なんて居るのかなぁ,と思うような箇条書きでまとめてみたいと思います.

  • メソッド呼び出しの高速化
    • コントロールフレームのサイズをもっとスリム化する
    • キャッシュをもっとアグレッシブに効かせる
    • simple CFUNC を作るためのフレームワークをきちんと作る
    • yield をなんとかする(キャッシュを効かせる等)
    • インラインメソッドキャッシュのアルゴリズムを検討する
    • refinement をもうちょっと考える
    • keyword 引数を高速に処理する
  • VM 構造
    • ep で例外処理のアンカーにしているのを cfp にする
    • iseq から Object を排除
    • 起動順序をもっとシンプルにする
    • アプリケーション組込みをもっとやりやすくする,ドキュメントを書く
  • 命令 / Compiler
    • レジスタマシン化を検討する
    • もうちょっと命令を整理する
    • optimizer をもうちょっとかけて実験する
    • tailcall いれる?
    • super をなんとかする(同一 Module の許可)
    • 複数ファイルをまとめる方法を作る(汎用 require フレームワーク)→ VFS 要る?
  • メモリ・オブジェクト管理 / GC
    • 型別のヒープ
    • mmap でまとめて確保,高速な bitmap marking
    • Reference counting String storage
    • Symbol GC を検討する
    • 世代別 GC を入れる
  • その他
    • ベンチマークをもうちょっとなんとかする
    • シグナル回りをもうちょっとなんとかする
    • C 拡張の規格をなんとかする
    • デバッガ,プロファイラサポートをもうちょっとなんとかする

沢山書いておきました.思いついたらあとで追加しておきますね.

こう見てみると,Ruby 2.0.0 は本当に保守的で,構想だけはあったけどドラスティックな機能はあんまり入ってないですね.

Ruby 2.1 までになんとかしたいなぁ.

では,また来年.良いお年を.

_24(Mon)

Ruby VM アドベントカレンダーの 24 日目です.

今日はクリスマスイブですね.ここまできちんと続けてこられてとても良かったと思います.

さて,今日はこれまでの高速化の話(文章はここで途切れている)

_23(Sun)

Ruby VM アドベントカレンダーの 23 日目です.

昨日の VM の高速化の話(文章はここで途切れている)

_22(Sat)

Ruby VM アドベントカレンダーの 22 日目です.

今日は,VM の高速化の話を(文章はここで途切れている)

_21(Fri)

Ruby VM アドベントカレンダーの 21 日目です.

スタックオーバーフローってありますよね.用意されたスタックを使い切ってしまうと出るエラーです.プログラムによって必要なスタックのサイズは違うものですが,Ruby 1.9 まではスタックのサイズを指定する方法がありませんでした.

そこで,Ruby 2.0 では次の 4 つの環境変数によって,スタックの深さを変えることが出来るようになりました.

  • RUBY_THREAD_VM_STACK_SIZE: スレッドを作る時に作成する VM スタックサイズ(デフォルト: 128KB (32bit CPU) or 256KB (64bit CPU))
  • RUBY_THREAD_MACHINE_STACK_SIZE: スレッドを作る時に作成するマシンスタックサイズ(デフォルト:512KB or 1024KB)
  • RUBY_FIBER_VM_STACK_SIZE: ファイバを作る時に作成する VM スタックサイズ(デフォルト:64KB or 128KB)
  • RUBY_FIBER_MACHINE_STACK_SIZE: ファイバを作る時に作成するマシンスタックサイズ(デフォルト:256KB or 512KB)

それぞれスレッドとファイバ用の,VM のスタックとマシンスタックのサイズを指定できます.

ただし,これらの値はあくまでヒント情報になっています.小さい値を指定しても,一定の値未満のサイズを指定することはできませんし,適当な数でアラインされます.そもそも OS によっては,スレッドのマシンスタックサイズを変更することが出来ない場合もあります.

これらの値はインタプリタ起動時にのみ有効です.インタプリタを起動後に環境変数の値を変化させても,インタプリタは変更を無視します.

ちなみに,1.9 の時代よりもスタックサイズが大きくなっています.これは,スタックサイズが大きくてまずいプログラムはあんまり無いからだろう,という判断です.もし,32bit CPU を使っており,Fiber(や Thread)を大量に作成するようなプログラムの場合,この環境変数を利用してチューニングすることを検討してみて下さい.


これらの設定を実行中に知るために,新しい定数 RubyVM::DEFAULT_PARAMS をこっそり付け加えました.

puts RubyVM::DEFAULT_PARAMS
#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
{:thread_vm_stack_size=>131072, :thread_machine_stack_size=>524288, :fiber_vm_stack_size=>65536, :fiber_machine_stack_size=>262144}

こんな感じで各値が格納されたハッシュオブジェクトが取れます.今後,何かしら値が追加されるかもしれません(されないかもしれません).なお,あくまで参考情報なので,この値を書き換えても,インタプリタの挙動には一切影響を与えません.

では,今日はこの辺で.

_20(Thu)

Ruby VM アドベントカレンダーの 20 日目です.

昨日は Module#prepend を紹介しました.include ではメソッド探索の順序で後ろに追加していたものを,前に追加するものでした.

復習のために,昨日のプログラムを再掲してみます.

class C0
  def m; p :C0; end
end

module M
  def m; p :M; super; end
end

class C < C0
  prepend M                # include だったのを prepend にしている
  def m; p :C; super; end
end

obj = C.new
obj.m

#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
:M
:C
:C0

C のオブジェクトにメソッド m を実行したとき,M#m が先に呼ばれていることがわかります.

昨日は「Module#prepend で,これまで出来なかったようなことができるようになります.試してみて下さい.」と結んだのですが,そもそもこれ,どんな時に使うんでしょうか.

一番ありそうな使い方としては,あるメソッドの開始と終了時に特定の処理をさせたい場合です.例えば,Array#each が呼ばれたとき,ログを出力するような例を記述してみます.

module EachLogger
  def each(*args, &block)
    puts "Enter `each'"
    super
    puts "Leave `each'"
  end
end

class Array
  prepend EachLogger
end


p :each
[1, 2, 3].each{}
p :find
[1, 2, 3].find(1)
p :find_all
[1, 2, 3].find_all{|e| (e % 2) == 1}

#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
:each
Enter `each'
Leave `each'
:find
:find_all
Enter `each'
Leave `each'

はい,このように each メソッドが実行されたとき,EachLogger によってログが出力されることがわかりました.

ちなみに,find メソッドでは each は使われていないことがわかります.逆に find_all メソッドでは使われていますが,これは Enumerable#find_all メソッドが each メソッドを呼び出しているからです.

このように,メソッドの開始と終了時に何かしらの処理を行うということを around と言ったりします.もちろん,開始時だけ,終了時だけ,ということも出来ます.


さて,先ほどは EachLogger という each メソッドをロギングするモジュールを作りましたが,どうせならあるクラスのすべてのメソッドのログを出力するものを作ってみましょう.

def prepend_method_logger klass
  mod = Module.new
  klass.instance_methods(false).each{|m|
    mod.module_eval %Q{
      def #{m}(*args)
        puts "Enter `#{m}'"
        super
        puts "Leave `#{m}'"
      end
    }
  }
  klass.module_eval{
    prepend mod
  }
end

class C
  def m; puts "C#m"; end
  def n; puts "C#n"; end
end

prepend_analyse_module C

obj = C.new
obj.m
obj.n

#=>
Enter `m'
C#m
Leave `m'
Enter `n'
C#n
Leave `n'

prepend_method_logger は指定されたクラスに定義されているメソッドすべてに対して EachLogger#each のように,ログを出力するメソッドを定義しています.module_eval を利用しているのが黒魔術的ですね.

実行結果で,C#m,C#n の実行時に,きちんとログが出力されていることがわかると思います.


さて,これを利用して,もうちょっと便利かもしれないツールを作ってみましょう.

あるクラスのメソッドが,どのような引数を受け付け,そして返値に何を返しているのか統計をとってみます.これを作ることで,意図しない引数を受け付けているかどうかがわかります.

def prepend_analyse_module klass
  $analyse_result = {} unless defined?($analyse_result)
  stat = $analyse_result[klass] = {}

  mod = Module.new
  klass.instance_methods(false).each{|m|
    stat[m] = [Hash.new{|h, k| h[k] = Hash.new(0)}, # args
               Hash.new(0)]                         # return
    src = %Q{
      def #{m}(*args)
        args.each.with_index{|e, i|
          $analyse_result[#{klass.name}][:#{m}][0][i][e.class] += 1
        }
        result = super
        $analyse_result[#{klass.name}][:#{m}][1][result.class] += 1
      end
    }
    mod.module_eval src
  }
  klass.module_eval{
    prepend mod
  }
end

class C
  def m foo, bar; puts "C#m"; end
  def n baz; puts "C#n"; end
end

prepend_analyse_module C

obj = C.new
obj.m 'a', 1
obj.m :sym, 1.2
obj.n /x/

require 'pp'
pp $analyse_result

#=>
C#m
C#m
C#n
{C=>
  {:m=>[{0=>{String=>1, Symbol=>1}, 1=>{Fixnum=>1, Float=>1}}, {NilClass=>2}],
   :n=>[{0=>{Regexp=>1}}, {NilClass=>1}]}}

色々面倒くさかったので,$analyse_result というグローバル変数に情報を全部詰め込むようにしてみました.

結果を見ると,C#m は 0 番目の引数に String および Symbol を 1 個ずつ,1 番目の引数に,... という情報がわかります.

これを有効にして,テストケースを走らせてみると,意図しない型が来ているかどうか,チェックできるかもしれません.

では,今日はこの辺で.

_19(Wed)

Ruby VM アドベントカレンダーの 19 日目です.

今日は,Ruby 2.0 で導入された Module#prepend についてご紹介します.

ご存じの通り,Ruby では Module を include することで,メソッド定義を拡張することが出来ます.

module M
  def m; p :M; end
end

class C
  include M
end

このとき,C オブジェクトはメソッド m を持っていることになります.

さてここで,C もメソッド m を持っているとき,どうなるか知っていますか?

module M
  def m; p :M; end
end

class C
  include M
  def m; p :C; end
end

C.new.m #=> :C

C#m と M#m では,C#m が使われていることがわかりました.実は,ancestors を見るとわかります.

# 上のコードの続きだと思って下さい.
p C.ancestors #=> [C, M, Object, Kernel, BasicObject]

Class#ancestors はクラスの階層構造を示しますが,これはそのままメソッドの探索順序にもなっています.C と M では C のほうが先に来るので,C#m が先にヒットするのでこちらが使われる,ということですね.

だめ押しに,C#m で super してみましょう.

module M
  def m; p :M; end
end

class C
  include M
  def m; p :C; super; end
end

C.new.m

#=>
:C
:M

C#m の super から M#m が呼ばれていることがわかります.


さて,C が M を include したとき,C -> M の順にメソッドが検索されることがわかりました.そして,C#m があれば,それが利用されます.

しかし,C#m よりも先に M#m を呼んで欲しい,という場合はどうすればいいでしょうか.実は 1 つ方法があります.Object#extend を使います.

class C0
  def m; p :C0; end
end

module M
  def m; p :M; super; end
end

class C < C0
  def m; p :C; super; end
end

obj = C.new
obj.extend M
obj.m

#=>
:M
:C
:C0

C オブジェクトである obj に extend メソッドによって M を拡張したため,見事に M,C(そして C0)の順にメソッドが呼ばれていることがわかります.よかったよかった.

が,これだと C.new で作成したオブジェクトすべてに対して Object#extend を呼ばねばならず不便です(そして,オブジェクトの生成が遅くなります).


そこで Ruby 2.0 からは Module#prepend という機能が追加されました.Module#include では include するクラスの後ろに加えていたのが,今度は prepend によって 前に追加する,という機能です.

では実際に使ってみましょう.

class C0
  def m; p :C0; end
end

module M
  def m; p :M; super; end
end

class C < C0
  prepend M                # include だったのを prepend にしている
  def m; p :C; super; end
end

obj = C.new
obj.m

#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
:M
:C
:C0

見事,obj.m は M#m,C#m(そして C0#m)の順に探索されるようになりました.一応,ancestors を見ておきましょうか.

# 前述のクラス定義があると思って下さい
p C.ancestors
#=> [M, C, C0, Object, Kernel, BasicObject]

というわけで,C の前に M があることが確認できると思います.

Module#prepend で,これまで出来なかったようなことができるようになります.試してみて下さい.

では,今日はこの辺で.

_18(Tue)

Ruby VM アドベントカレンダーの 18 日目です.

今回は 2.0 から試験的に導入された Debug Inspector API のご紹介をしようと思います.なお,この機能も例によって実験中ですので,あとで変わるかもしれませんし,無くなるかもしれません.フィードバック,お待ちしております.

さて,Debug Inspector API とは何かというと,ずばりスタックフレームの情報を取り出すことです.っていうと,caller_locations でいいじゃん,という気がしますが,これはスタックフレームの任意のフレームの Binding オブジェクトが取り出せます.つまり,幻のメソッド Binding of caller って奴です.ただ,利用方法はちょっと複雑になっています.また,パフォーマンスが凄い遅いので,普通のプログラムの実行では使えないんじゃないかと思います.というか,使わないで下さい.あくまで,デバッガ用です.濫用厳禁.

Debug Inspector API と関係する宣言を include/ruby/debug.h から引用します.

/* debug inspector APIs */
typedef struct rb_debug_inspector_struct rb_debug_inspector_t;
typedef VALUE (*rb_debug_inspector_func_t)(const rb_debug_inspector_t *, void *);

VALUE rb_debug_inspector_open(rb_debug_inspector_func_t func, void *data);
VALUE rb_debug_inspector_frame_self_get(const rb_debug_inspector_t *dc, long index);
VALUE rb_debug_inspector_frame_class_get(const rb_debug_inspector_t *dc, long index);
VALUE rb_debug_inspector_frame_binding_get(const rb_debug_inspector_t *dc, long index);
VALUE rb_debug_inspector_frame_iseq_get(const rb_debug_inspector_t *dc, long index);
VALUE rb_debug_inspector_backtrace_locations(const rb_debug_inspector_t *dc);

まず,利用するときは rb_debug_inspector_open() を,関数ポインタ func と共に呼び出します.rb_debug_inspector_open() は,現在のコンテキストに対応した rb_debug_inspector_t オブジェクト(ここでは,デバッグコンテキストと呼びます)を作成し,func は生成されたデバッグコンテキストへのポインタと,rb_debug_inspector_open() の第二引数 data と共に呼ばれます.

だいたい,こんな感じになります.

VALUE
func(const rb_debug_inspector_t *dc, void *data)
{
  ...
}


  ... /* デバッグしたい箇所で */
  rb_debug_inspector_open(func, some_data_ptr);
  ...

デバッグコンテキストへのポインタ dc によって,現在のフレームの情報にアクセスできます.なお,dc は func が呼び出されている間のみ有効であり,func から返った後は dc は解放されていまいますのでご注意下さい.

まずは,caller_locations を取り出しましょう.rb_debug_inspector_backtrace_locations(dc) とすれば取り出せます.これを取り出すことで,スタックフレームの深さがわかります(今思えば,サイズを返す API を用意しておけばよかった.ごめん).これで,スタックの深さが `depth' であることがわかったとします.

n フレーム目(ただし,0 <= n < depth)のフレームの情報を取り出したいときは,rb_debug_inspector_frame_???_get(dc, n) として取り出します.??? に,取り出したい情報を書き込みます.これには次の種類があります.

  • self - そのフレームの self
  • class - そのフレームを実行しているメソッドの定義されているクラス(モジュール)
  • binding - そのフレームの binding
  • iseq - そのフレームの RubyVM::InstructionSequence オブジェクト

ただし,binding や iseq は C メソッドなどで nil である可能性があります.class は TracePoint#defined_class で取り出すクラス(モジュール)と同様に特異メソッドの場合,特異クラスを返します.

さて,これがあると色々といやらしいことができる気がしませんか.でもあんまり濫用しないで下さいね.大事なことなので繰り返しておきますが,この機能も例によって実験中ですので,あとで変わるかもしれませんし,無くなるかもしれません.フィードバック,お待ちしております.

では,今日はこの辺で.

_17(Mon)

Ruby VM アドベントカレンダーの 17 日目です.

DTrace が出来るようになりました.Aaron たちのおかげです.紹介記事が今度書かれる予定なので,詳細はそっちを参照して下さい(実装していないのでよく知らない).

DTrace の導入の仕組みにはあまり納得していないし,あのトレースの情報が正しいとは思っていないので(多分,ちょっと遅くなるし,なんかコードが汚い),気力があるときに,あの辺はざっくり入れ替えたいと思っています.

では,今日はこの辺で(手抜き).

_16(Sun)

Ruby VM アドベントカレンダーの 16 日目です.

せっかく誰も見ないようなコアな話をする場所なので,誰も興味なさそうな TracePoint の実装について述べます.

vm_trace.c にあります.ここには set_trace_func を行う rb_add_event_hook() などがまず実装されており,そのあとそれを使って set_trace_func が実装され,それと並行に TracePoint API が実装されている,という感じです.

Trace を安全に無効にする処理に,ちょっと工夫がありますが,もうちょっと綺麗になるんじゃないかと思っています.

まぁ,あとは読めば判ります.簡単です.もしわからない,という場所がありましたらコメント下さい.

では,今日はこの辺で(手抜き).

_15(Sat)

Ruby VM アドベントカレンダーの 15 日目です.

昨日まで,3 回に渡って TracePoint の紹介をしてきました.今日は,TracePoint の機能を C 拡張から利用するための C API についてご紹介します.

include/ruby/debug.h というファイルをご覧下さい.「/* TracePoint APIs */」というコメントから始まる箇所がそれに当たります.

短いですので,全部引用してみましょう.

VALUE rb_tracepoint_new(VALUE target_thread_not_supported_yet,
                        rb_event_flag_t events, 
                        void (*func)(VALUE, void *), void *data);
VALUE rb_tracepoint_enable(VALUE tpval);
VALUE rb_tracepoint_disable(VALUE tpval);
VALUE rb_tracepoint_enabled_p(VALUE tpval);

typedef struct rb_trace_arg_struct rb_trace_arg_t;
rb_trace_arg_t *rb_tracearg_from_tracepoint(VALUE tpval);

VALUE rb_tracearg_event(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_lineno(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_path(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_method_id(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_defined_class(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_binding(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_self(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_return_value(rb_trace_arg_t *trace_arg);
VALUE rb_tracearg_raised_exception(rb_trace_arg_t *trace_arg);

TracePoint.new に相当するのが rb_tracepoint_new() です.引数に「トレースを有効にするスレッド」,「トレースしたいイベント」,「トレースに相当する C 関数のポインタ func」および「func を呼ぶときに渡すポインタ」を意味します.返値は TracePoint オブジェクトになります.まぁ,わかりますよね.

スレッドの指定するべきパラメータ名が target_thread_not_supported_yet となっていますが,テストしていないので未サポートということにしています.誰かテストして下さい.イベント指定の events は,イベントを表す数値の論理和になります.

現在の trunk では,指定できるイベントは include/ruby/ruby.h に下記のように定義されています.それぞれ,名前を見れば何を意味するかわかりますよね.

/* traditional set_trace_func events */
#define RUBY_EVENT_NONE      0x0000
#define RUBY_EVENT_LINE      0x0001
#define RUBY_EVENT_CLASS     0x0002
#define RUBY_EVENT_END       0x0004
#define RUBY_EVENT_CALL      0x0008
#define RUBY_EVENT_RETURN    0x0010
#define RUBY_EVENT_C_CALL    0x0020
#define RUBY_EVENT_C_RETURN  0x0040
#define RUBY_EVENT_RAISE     0x0080
#define RUBY_EVENT_ALL       0x00ff

/* for TracePoint extended events */
#define RUBY_EVENT_B_CALL          0x0100
#define RUBY_EVENT_B_RETURN        0x0200
#define RUBY_EVENT_THREAD_BEGIN    0x0400
#define RUBY_EVENT_THREAD_END      0x0800
#define RUBY_EVENT_TRACEPOINT_ALL  0xFFFF

/* special events */
#define RUBY_EVENT_SPECIFIED_LINE 0x10000
#define RUBY_EVENT_SWITCH         0x20000
#define RUBY_EVENT_COVERAGE       0x40000

func と data も勘でわかるんじゃないかと思います.ちなみに,func の第一引数は,生成した TracePoint オブジェクトになります.これも,TracePoint.new に渡すブロックと同じですね.

さて,作成したトレースはまだ有効になっていないので rb_tracepoint_enable() によって有効にします.無効にするには rb_tracepoint_disable() ですね.

さて,指定したイベントが発生すると,func が TracePoint オブジェクトと data とともに呼ばれます.TracePoint#event などを実行したいわけですが,ここで一手間書ける必要があります.rb_tracearg_from_tracepoint() に TracePoint オブジェクトを渡して rb_trace_arg_t へのポインタを取得して下さい.ここで得られたポインタを使って rb_tracearg_event() などを呼び出すことで,情報を取得することが出来ます.

具体的には,次のようになるんじゃないかと思います.

void
func(VALUE tpobj, void *data)
{
  rb_trace_arg_t *trace_arg = rb_tracearg_from_tracepoint(tpobj);
  VALUE event_sym = rb_tracearg_event(trace_arg);
  ...
}

あとは,使い方わかりますよね,勘で.

というわけで,駆け足ですが,TrcaePoint を C 拡張から使うための C API をご紹介しました.これで高速なプロファイラとか誰か作って.

おっと,大事なことを言い忘れていました.debug.h の先頭を見ると書いてある

/* Note: This file contains experimental APIs. */
/* APIs can be replaced at Ruby 2.0.1 or later */

に注意して下さい.この辺の C API は,まだ,作ったばかりなので,色んなまずい点があるんじゃないかと思っているんですが,実際いくつかすでに見つかっています.なので,今作る人はあとで変わったときに追従する覚悟で使ってやって下さい.ゴメンね.また,なんか都合の悪い部分を見つけましたらコッソリ教えて下さい.

では,今日はこの辺で.

_14(Fri)

Ruby VM アドベントカレンダーの 14 日目です.

今日は,最近紹介している TracePoint の細かい使い方の紹介をしようと思います.

昨日の記事では TracePoint.trace を使いました.set_trace_func と同様に,このメソッド終了時から設定したトレースが有効になります.しかし,普通のオブジェクトのように,TracePoint.new によって TracePoint オブジェクトを作成することが出来ます.作成後は,まだ有効ではないので,TracePoint#enable としてトレースを有効にします.ちなみに,TracePoint.new の引数は TracePoint.trace と同じです.

trace = TracePoint.new(events...){|tp| ... trace ... }
... # まだトレースは有効じゃない
trace.enable
... # この範囲のみトレースは有効
trace.disable

つまり,TracePoint.trace というのは,TracePoint.new して,それを enable するところを一息で行う便利メソッドというわけです.

ちなみに TracePoint#enable はブロックを与えることができ,そのブロックを実行中のみトレースを有効にする,という使い方が出来ます.

trace = TracePoint.new(events...){|tp| ... trace ... }
... # まだトレースは有効じゃない
trace.enable{
  ... # この範囲のみトレースは有効
}

TracePoint#disable も,同様にブロックを与えることができ,そのブロックの範囲だけトレースを無効にすることができます.


さて,ではどのイベントがトレースできるか,ですが基本的には set_trace_func でできたトレース+αとなっています.TracePoint の rdoc から引用します.

  • :line - execute code on a new line
  • :class - start a class or module definition
  • :end - finish a class or module definition
  • :call - call a Ruby method
  • :return - return from a Ruby method
  • :c_call - call a C-language routine
  • :c_return - return from a C-language routine
  • :raise - raise an exception

ここまでは set_trace_func と同じで,

  • :b_call - event hook at block entry
  • :b_return - event hook at block ending
  • :thread_begin - event hook at thread beginning
  • :thread_end - event hook at thread ending

これらが TracePoint から新たに可能になったトレースできるイベントになります.まぁ,意味は読めばわかりますよね.

シンボルで渡せるようにした都合上,"c-call" のようなイベント名は :c_call にようになっています.


さて,次に TracePoint.new のブロックに渡ってくるブロックパラメータは一体なんなのか,ということを調べてみます.まず,クラスを見てみます.

trace = TracePoint.trace{|tp|
  p tp.class
  exit
}
#=> TracePoint

どうやら,これも TracePoint オブジェクトのようです.

inspect して調べてみましょう.

trace = TracePoint.trace{|tp|
  p [:tp, tp.object_id, tp]
}
p [:trace, trace.object_id, trace]
#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
[:tp, 1476430, #<TracePoint:c_return `trace'@t.rb:1>]
[:tp, 1476430, #<TracePoint:line@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `object_id'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_return `object_id'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `p'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `hash'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_return `hash'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_return `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_return `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_call `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_return `inspect'@t.rb:4>]
[:tp, 1476430, #<TracePoint:c_return `inspect'@t.rb:4>]
[:trace, 1476430, #<TracePoint:enabled>]
[:tp, 1476430, #<TracePoint:c_return `p'@t.rb:4>]

ちょっとわかりづらくなっちゃってますが,実は TracePoint.new で作成されるオブジェクトとブロックパラメータ(ここでは tp)は同じオブジェクトです(object_id が同じなのがわかると思います).トレース実行ごとに情報へアクセスするためだけのオブジェクトを作成するのは嫌だなぁ,と思ってこういう設計にしています.

同じオブジェクトでありながら,inspect の結果が,トレース実行中と,トレースを実行していない時で違うのがわかります.トレース実行中は,TracePoint#inspect は何のイベントでトレース中であるか,わかりやすく表示しています.トレース実行中でないときは,その TracePoint オブジェクトで指定されるトレースが有効であるかどうかを示しています(この場合,enable と表示していますね).

なお,TracePoint#enabled? メソッドによって,そのトレースが有効であるかどうかを示しています.


最後に,トレース中にどんな情報が取れるかを紹介します.

  • TracePoint#event - なんのイベントが発生したか返します
  • TracePoint#lineno - イベントが発生した行番号を返します
  • TracePoint#path - イベントが発生したパス名を返します
  • TracePoint#defined_class - イベントが発生したメソッドが定義されているクラス(モジュール)を返します
  • TracePoint#method_id - イベントが発生したメソッド名を返します
  • TracePoint#binding - イベントが発生した箇所の Binding オブジェクトを返します
  • TracePoint#return_value - イベントが発生した時の返値を返します
  • TracePoint#raised_exception - イベントが発生した時の例外オブジェクトを返します

event から binding までは,set_trace_func で取れた情報とほぼ同じです.

TracePoint#return_value は,:return, :c_return, :b_return イベントの時のみ利用できるメソッドで,return が発生したときの返値を渡します.その他のイベント発生中にこのメソッドを呼んでも例外が発生します.

TracePoint#raised_exception は,:raise イベントの時のみ利用できるメソッドで,どの例外が発生したのかを返します.TracePoint#return_value と同様に,その他のイベント発生中にこのメソッドを呼んでも例外が発生します.

さて,TracePoint#defined_class は,set_trace_func で利用できる klass 情報とは若干違います(上記で「ほぼ」と言ったのはこの点が異なるためです).set_trace_func では,特異メソッドが呼ばれたときに klass にはオリジナルのクラスが渡ってきましたが,TracePoint#defined_class では特異クラスが返るようになっています.これによって,このメソッドが特異クラスかどうかわかるようになっています.

さて,trace = TracePoint.trace{|tp| ...} としたとき,trace と tp が,実は同じということでしたが,では トレースを実行していないときに,trace.event などとするとどうなるでしょうか.答えは例外が発生する,でした.

さて,3 回でトレースポイントを紹介しました.色々と面白い使い方ができる新機能だと思いますので,何かしら使ってみて下さい.

では,今日はこの辺で.

_13(Thu)

Ruby VM アドベントカレンダーの 13 日目です.

昨日は TracePoint の利用例だけ紹介しました.コードを再掲します(といって行を稼ぐ).

def m
  p :hello
  p :world
end

set_trace_func(proc{|event, file, line, id, binding, klass|
  puts "#{file}:#{line} #{id}" if event == 'line'
})

m

#=>
t.rb:10 
t.rb:2 m
:hello
t.rb:3 m
:world

これを TracePoint で書き換えた版は次に.

def m
  p :hello
  p :world
end

trace = TracePoint.trace(:line){|tp|
  puts "#{tp.path}:#{tp.lineno} #{tp.method_id}"
}

m

#=>
t.rb:10 
t.rb:2 m
:hello
t.rb:3 m
:world

トレースを設定しているところは次の箇所ですね.

set_trace_func(proc{|event, file, line, id, binding, klass|
  puts "#{file}:#{line} #{id}" if event == 'line'
})

trace = TracePoint.trace(:line){|tp|
  puts "#{tp.path}:#{tp.lineno} #{tp.method_id}"
}

さて,違いをゆっくり見て行きましょう.

まず,登録のインターフェースに着目してみます.

  • set_trace_func() は関数的メソッドですが,TracePoint.trace は TracePoint クラスのクラスメソッドになっています.
  • set_trace_func では Proc オブジェクトを陽に渡していますが,TracePoint.trace ではブロックを渡しています.こっちのほうがタイピング量が少ないかもしれませんね.
  • TracePoint.trace では,引数にシンボルで :line と渡しています.これは,:line イベントだけトレースする,という指定になります.
  • set_trace_func は返値を見ませんが(トレースとして登録した Proc オブジェクトが返ります),TracePoint.trace の返値は,TracePoint オブジェクトでして,trace という変数で登録しています

今回の例ではこの返値である TracePoint オブジェクトは使っていませんが,trace.disable とすることで,このトレースだけ無効にすることができます.ちなみに,こんな感じです.

trace = TracePoint.trace{...}
...
trace.disable # 以降はトレースが無効になる
...

次に,トレースとして渡すブロックを見てみます.

  • ぱっと見てわかるのが,ブロックパラメータの違いです.set_trace_func では event, file, line, id, binding, klass と,必要そうなオブジェクトが全部渡ってきますが,TracePoint.trace では tp だけです.
  • 次の行では set_trace_func ではこの line などを直接使って出力を作成していますが,TracePoint.trace では tp.lineno と,tp へのメソッド呼び出しを行うことで行番号を得て出力を作成しています.

というわけで,大きな違いというか特長は,次の 4 つになります.

  • TracePoint.trace は返値が重要である(無効化できる)
  • TracePoint.trace にはブロックを渡す
  • TracePoint.trace にはトレースしたいイベントを指定する
  • TracePoint.trace では,欲しい情報はブロックパラメータ(tp)へのメソッド呼び出しで取得する

さて,昨日言っていた set_trace_func の問題点は解決したでしょうか.チェックしてみましょう.

  • (1) 呼び出すイベントの種類を制限できない

TracePoint.trace では,引数で :line のように,トレースしたいイベントの種類を指定できるので解決しています.

  • (2) 複数のトレースを,それぞれ有効にしたり無効にしたりできない

TracePoint.trace によって得られる TracePoint オブジェクトの TracePoint#disable メソッドによって,このトレースのみを無効に出来ます.

  • (3) 遅い

遅い理由はいくつかありました.Proc オブジェクトを呼び出すコストがかかる,というものですが,(1) で興味のないイベントでは Proc オブジェクト呼ばれないので,Proc の起動コストの問題は若干解決しています.また,Binding オブジェクトを必ず作成してしまう,という問題は,tp.binding というメソッドによって Binding オブジェクトの作成を指示するまで作成されないので,これも問題ありません.

実際にためしてみましょう.10万回のトレース付きのメソッド呼び出しです.

require 'benchmark'

def m
  a = 1
  b = a
  a = b
end

max = 100_000

Benchmark.bm{|x|
  x.report{
    set_trace_func(proc{|event, file, line, id, binding, klass|
      if event == 'line'
        # do something
      end
    })
    max.times{m}
    set_trace_func(nil)
  }
  x.report{
    trace = TracePoint.trace(:line){|tp|
      # do something
    }
    max.times{m}
    trace.disable
  }
}

実行結果です.

ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
       user     system      total        real
   6.755000   0.016000   6.771000 (  7.176412)
   0.733000   0.000000   0.733000 (  0.775598)

10 倍ほど速くなっています.というか,set_trace_func が遅すぎただけ,という言い方が出来るかもしれません.

  • (4) イベントがこれ以上増やせない

新規 API なので,イベントを増やしても怒られません.また,TracePoint.trace では,トレースしたいイベントを指定するようになっているので,意図しないイベントが来て困るようなトレースが書けないようになっています(ちなみに,無引数では全イベントを受け付けるようになっていますが,そこは自己責任で.今後,増える可能性があることを考慮して書いて下さい).こんな感じで,互換性の問題も解決しています.

というわけで,TracePoint では,set_trace_func に含まれていた 4 つの問題を解決していることがわかります.

さて,今日も長くなってしまったので,TracePoint の細かい紹介は明日に回すことにします.

では,今日はこの辺で.

_12(Wed)

Ruby VM アドベントカレンダーの 12 日目です.

今日は Ruby 2.0 から導入された TracePoint についてご紹介します.

従来より,set_trace_func(proc_object) というインターフェースがありました.るりまから引用します.

Ruby インタプリタのイベントをトレースする Proc オブジェクトとして指定された proc を登録します。 proc に nil を指定した場合でトレースをオフにします。ブロックを指定された場合はそのブロックをトレースする Proc オブジェクトとして登録します。

登録したトレース(Proc オブジェクト)は,イベントが発生するたびに呼ばれます.イベントの種類は次のとおりです(るりまから引用します).

  • "line": 式の評価。
  • "call": メソッドの呼び出し。
  • "return": メソッド呼び出しからのリターン。
  • "c-call": Cで記述されたメソッドの呼び出し。
  • "c-return": Cで記述されたメソッド呼び出しからのリターン。
  • "class": クラス定義、特異クラス定義、モジュール定義への突入。
  • "end": クラス定義、特異クラス定義、モジュール定義の終了。
  • "raise": 例外の発生。

1.9 では,これを利用して,debug.rb や profile.rb などが実装されていました.

(ちなみに,Thread#set_trace_func などもあるんですが,今回は省略)


set_trace_func は(怪しいことをするには)便利な機能なのですが,いくつか問題がありました.

  • (1) 呼び出すイベントの種類を制限できない
  • (2) 複数のトレースを,それぞれ有効にしたり無効にしたりできない
  • (3) 遅い

(1) は,例えば line イベントだけ処理したいな,という場合も,すべてのイベントでトレースが呼ばれてしまうため,line だけの場合は,例えば次のように書く必要がありました.

set_trace_func(proc{|event, file, line, id, binding, klass|
  case line
  when 'line'
    # ...
  else
    # 他は無視する
  end
})

大きな問題ではないですが,ちょっと面倒くさいですね.

(2) の「複数のトレースを,それぞれ有効にしたり無効にしたりできない」という問題ですが,複数のトレースを追加することはできます.ただ,それらを個別に無効にする方法はなく,全て無効にする方法しかありません.

set_trace_func(proc{|*args| p [1, args]})
set_trace_func(proc{|*args| p [2, args]})

p :hello

set_trace_func(nil) # ここで無効にする

p :world

#=>
c:/ko1/ruby/clean-trunk/bin/ruby: warning: -K is specified; it is for 1.8 compatibility and may cause odd behavior
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
[1, ["c-return", "t.rb", 1, :set_trace_func, #<Binding:0x6fbf1c>, Kernel]]
[1, ["line", "t.rb", 2, nil, #<Binding:0x6fbb84>, nil]]
[1, ["c-call", "t.rb", 2, :proc, #<Binding:0x6fb9a4>, Kernel]]
[1, ["c-return", "t.rb", 2, :proc, #<Binding:0x6fb580>, Kernel]]
[1, ["c-call", "t.rb", 2, :set_trace_func, #<Binding:0x6faf68>, Kernel]]
[2, ["c-return", "t.rb", 2, :set_trace_func, #<Binding:0x6fa874>, Kernel]]
[2, ["line", "t.rb", 4, nil, #<Binding:0x6fa324>, nil]]
[2, ["c-call", "t.rb", 4, :p, #<Binding:0x6f9d84>, Kernel]]
[2, ["c-call", "t.rb", 4, :hash, #<Binding:0x6f96b8>, Kernel]]
[2, ["c-return", "t.rb", 4, :hash, #<Binding:0x6f8cb8>, Kernel]]
[2, ["c-call", "t.rb", 4, :inspect, #<Binding:0x6f81b4>, Symbol]]
[2, ["c-return", "t.rb", 4, :inspect, #<Binding:0x6f37f4>, Symbol]]
:hello
[2, ["c-return", "t.rb", 4, :p, #<Binding:0x6f331c>, Kernel]]
[2, ["line", "t.rb", 6, nil, #<Binding:0x6f2d18>, nil]]
[2, ["c-call", "t.rb", 6, :set_trace_func, #<Binding:0x6f228c>, Kernel]]
:world

1番目と2番目のトレースがそれぞれ実行されているのがわかると思いますが,1つめだけ無効にする,という方法がないということがわかります.

(3) の遅いというのは,言わずもがななのですが,もっとも許されざるべき大罪です.プロファイラやデバッガを作りたい,などという用途では,トレースは出来るだけ対象プログラムに影響を与えないことが求められます.

そもそも,この機能はイベントが発生するたびにトレースを実行しますので,その起動コストがかかります.しかも,(1) で述べた通り,興味のないイベントが発生しても,毎回トレースを実行させます.例えば line イベントだけトレースしたい,という場合も,他のイベントは遠慮無く発生しますんで,無視するだけでもコストがかかることになります.

起動コストについて,もう少し細かくみてみますと,そもそも Proc オブジェクトを call するコストがかかります.もちろん,長い時間かかるものでもありませんが,チリも積もれば山となります.実は,メソッド呼び出しよりも遅いです(これは,今後の課題です).また,ブロックの引数に渡されるオブジェクトの準備が大変です.とくに,イベントが発生した箇所の Binding オブジェクトを生成しますが,これが重いです.デバッガ用途で利用するときは,Binding オブジェクトが必要になりますが,プロファイラ用途では不要な場合が多いです.というわけで,必要だったり必要じゃなかったりする引数の準備を,毎回丁寧にしているので遅いです.

実は,仕様策定および実装者としてもう一つ問題があって,それが (4) イベントがこれ以上増やせない,という互換性の問題なのでした.新しいイベントを追加しようと考えたとき,先ほどのような,興味のないイベントを無視するようなプログラムばかりならよいのですが,現在の 8 つのイベントでトレースが実行されることしか考慮していないプログラムが想定され,互換性に問題が生じます.というか,イベントを追加したいなーと思ってたのですよね,ぶっちゃけ.


さて,set_trace_func の問題を解決するための新たな機能が TracePoint になります.

例えば,行が実行されたときにファイル名と行番号,そしてメソッド名を表示するプログラムは,set_trace_func で書くと下記のようになります.

def m
  p :hello
  p :world
end

set_trace_func(proc{|event, file, line, id, binding, klass|
  puts "#{file}:#{line} #{id}" if event == 'line'
})

m

#=>
t.rb:10 
t.rb:2 m
:hello
t.rb:3 m
:world

これを TracePoint で書き換えると次のようになります.

def m
  p :hello
  p :world
end

trace = TracePoint.trace(:line){|tp|
  puts "#{tp.path}:#{tp.lineno} #{tp.method_id}"
}

m

#=>
t.rb:10 
t.rb:2 m
:hello
t.rb:3 m
:world

あまり,違いはないような気もしますが,細かいところが変わっています.ちょっと記事が長くなってしまったので,具体的な TracePoint の紹介は明日に行うことにします.

では今日はこの辺で.

_11(Tue)

Ruby VM アドベントカレンダーの 11 日目です.

昨日は Flonum についてご紹介しまいた.今日は Flonum の実装をご紹介します.

詳細は 2008 年に論文を書いたのでご参照下さい.日本語です.

スライドのほうが見やすいです.多分.あと,ここでは Fixnum を 2 bit,Float を 1 bit のタグ長としましたが,Ruby 2.0 で実装された Flonum では,Float を 2 bit,Fixnum を 1 bit(これまでと変わらず)というように実装し直しています.

に,最終的にどうしたか,スライドにまとめてあります.

...だけだとあんまりなので,概要です.

  • Fixnum と同じように管理したいので,VALUE に即値を埋め込む,ということを考えます.
  • IEEE754 倍精度表現は 64 bit なので,これを VALUE に埋め込むことにします(64bit 環境の場合,VALUE は 64 bit なので).
  • ただし,VALUE が Float である,と区別する必要があるので,なんとかしたい.Fixnum は下位 1 bit を使って Fixnum かそれ以外を判別しているので,2 bit を使って Flonum かそれ以外を判断するようにしよう.
  • となると,IEEE754 の倍精度表現を 62 bit で表現しなければならないけど,情報を落としたくないのでどうしよう.

というところから,ハックが入っていくわけですが,結果的には exponential のところを 2 bit 潰して,タグを埋め込むようにしました.で,2 bit 潰すと情報が失われてしまうため,決めうちをした範囲以外(昨日紹介した 1.72723e-77 より大きく 1.15792e+77 以下という範囲.あれ,未満だったかな? ちょっといい加減)だったら,Flonum を諦める,というようになっています.で,その範囲というのが exponential でいうと(以下略),という血湧き肉躍るハックになるわけです.あとは素晴らしいビット演算の世界に落とせるわけですが,その辺の最適化は shinh さんに提案して頂きました.ゴルファー凄い.

具体的には,include/ruby/ruby.h の VALUE rb_float_new(double),double rb_float_value(VALUE) あたりです.この説明にフラストレーションがたまった方は(多分たまると思う),ぜひコードを読んでみて下さい.短いからわかりやすいですよ.

では今日はこの辺で.

_10(Mon)

Ruby VM アドベントカレンダーの 10 日目です.

Ruby 2.0 から,ポインタが 64bit である環境では Flonum を導入して Float(浮動小数点数)の計算の高速化を行っています.厳密にいうと,メモリ管理の時間が減っています.

Ruby 1.9 までは,Float の計算結果ごとに,別々のオブジェクトを返していました.

2.times{
  p((1.1 + 1.2).object_id)
}
#=> 5343490
#=> 5343340

計算結果である 2.3 というオブジェクトに対して,それぞれ別々の object_id が与えられていることがわかるかと思います.つまり,別々のオブジェクトになっています.

これは,全ての Float の計算に当てはまりまして,例えば 1.1 + 1.2 + 1.3 ですと,まずは (1.1+1.2) の結果である 2.3 を作りまして,それに 1.3 を足して 3.6 というオブジェクトを返します.つまり,最終的に欲しいオブジェクトは 3.6 だけなのですが,ここでは 2.3 という余計なオブジェクトが作られていることになります.

確認してみましょう(下記では,オブジェクトスペースに,すでに別の Float があるので,1.x ではなく,100.x に変更しています).

x = 100.1 + 100.2 + 100.3
ObjectSpace.each_object(Float){|f|
  p f if f > 100
}
#=>
300.6
200.3
100.3
100.2
100.1
Infinity
1.7976931348623157e+308

余計なオブジェクトである 200.3 が生成されていることがわかります.この例では 1 つだけの余計なオブジェクトですが,a0 + a1 + ... + aN のような N 回の足し算をすると,N-2 個の余計なオブジェクトが作られてしまうことになります.

このように,Float オブジェクトをいちいち作っていると,(1) Float オブジェクト生成のオーバヘッド (2) GC のオーバヘッドが問題になります.とくに,数値計算を頻繁に行っているアプリケーションの場合,基本的に Float オブジェクトは短命であるため,(2) が問題になることが多いです.

では,整数はどうかというと,Fixnum の範囲では余計なオブジェクトは作りません.というか,Fixnum オブジェクトは GC 対象のオブジェクトではありません.確認してみましょう.

n = 1 + 2 + 3
ObjectSpace.each_object(Fixnum){|i|
  p i #=> 何も出力しない
}

これは,Ruby 処理系(MRI)が,Fixnum を特別扱いしているためです(RHG 第2章 オブジェクト を参照).

特別扱いしているので,同じ数値の Fixnum オブジェクトは,同じ object_id を持ちます.先ほどと同じように確認してみましょう.

2.times{
  p((11 + 12).object_id)
}
#=> 47
#=> 47

どちらも 47 になりました.つまり,33 という Fixnum オブジェクトの object_id は 33 ということです(この数値自体は,処理系によって変わることがあります.例えば,64bit でビルドされた Ruby では,67 になります).

また,Fixnum を特別扱いしているので,Fixnum オブジェクトは GC 対象のオブジェクトとしては作成されず,いくら Fixnum オブジェクトを利用しても,GC は起こりません.

GC が起こらないことを確認してみましょう.

puts "GC count (before): #{GC.count}"
10_000_000.times{|i|
  n = i + i
}
puts "GC count (after): #{GC.count}"
#=>
GC count (before): 4
GC count (after): 4

GC 回数が 4 回のまま変わっていないので,この繰り返しを行っても GC が発生していないことがわかります.ちなみについでに,Float がどうなるか,一応確認しておきましょう.

puts "GC count (before): #{GC.count}"
10_000_000.times{|i|
  n = 1.1 + 1.2
}
puts "GC count (after): #{GC.count}"
#=>
ruby 1.9.3p332 (2012-11-15 revision 37660) [i386-mswin32_100]
GC count (before): 1
GC count (after): 776

775 回 GC していることがわかります.

このように,Fixnum は特別扱いされているが,Float は特別扱いされていない,というのが性能を大きく分ける原因でした.

そこで,Float オブジェクトを特別扱いして Float の計算を速くしよう,というのが Flonum です.Fixnum がどう特別扱いされて,どのような性質を持っていたか,というのをまとめると次のようになります.

  • 同じ値は同じ object_id を持つ
  • いくら作っても GC は走らない(GC とは独立に管理される)
  • なので,ObjectSpace には出現しない

では,Flonum を実装した処理系で,さらに 64 bit 環境での Float の特長を確認してみましょう.

s = Hash.new(0)
puts "GC count (before): #{GC.count}"
10_000_000.times{|i|
  n = 1.1 + 1.2
  s[n.object_id]+=1
}
puts "GC count (after): #{GC.count}"
p s
ObjectSpace.each_object(Float){|f| p f}
#=>
ruby 2.0.0dev (2013-01-24 trunk 38925) [x86_64-linux]
GC count (before): 4
GC count (after): 4
{5404319552844594=>10000000}
NaN
Infinity
1.7976931348623157e+308
2.2250738585072014e-308

この結果を見ると,

  • (1) 同じ値は同じ object_id(5404319552844594)を持つ
  • (2) いくら作っても GC 回数は 4 のままなので GC は走っていない
  • (3) ObjectSpace には 2.3 は出現しない

ということがわかり,ほぼ Fixnum と同じ性質を持つことがわかります.

ただ,Fixnum とちょっと違うのが,ObjectSpace で Float のオブジェクトが見えていますね.

NaN
Infinity
1.7976931348623157e+308
2.2250738585072014e-308

が見えています.実は,Flonum では全ての Float ではなく,ある範囲の Float オブジェクトを特別扱いし,その範囲外の Float オブジェクトは従来どおりの,Float オブジェクトを作ります.その特別扱いする範囲とは,+0.0,および,1.72723e-77 より大きく 1.15792e+77 以下の範囲です(細かい数値は違うかも).多分,普通の人はこの範囲を超える値をあまり使わないと思いますので,だいたい Flonum が使われるんじゃないかと思います.

これに伴い,Float,Fixnum ともに freeze されるようになりました.

p 1.frozen?
p 1.2.frozen?
#=>
true
true

さて,では肝心の性能を見てみましょう.

require 'benchmark'
Benchmark.bm{|x|
  x.report{
    10_000_000.times{|i|
      n = 1.1 + 1.2
    }
  }
}

#=>
ruby 1.9.3p385 (2013-02-06 revision 39113) [x86_64-linux]
       user     system      total        real
   1.760000   0.000000   1.760000 (  1.761259)

ruby 2.0.0dev (2013-01-24 trunk 38925) [x86_64-linux]
       user     system      total        real
   1.210000   0.000000   1.210000 (  1.209062)

2.0 のほうが速くなっていることがわかると思います.

では今日はこの辺で.

_9(Sun)

Ruby VM アドベントカレンダーの 9 日目です.

昨日の記事では,ObjectSpace::InternalObjectWrapper というものが登場しました.さて,これはいったいなんでしょうか.

ObjectSpace.reachable_objects_from(obj) は,obj が直接参照するすべてのオブジェクトを返すのですが,中には Ruby からは触るべきではないオブジェクトが存在します.そのようなものの場合,一度 InternalObjectWrapper で wrap してから返すようにしています.

ちなみに,触るべきでないオブジェクトは,通常のメソッドの返値などで取ることが出来ないのはもちろんのこと,ObjectSpace.each_object でも取ることは出来ないようになっています.

実際に,どのようなものがあるか,試してみましょう.

require 'objspace'
require 'pp'

iseq = RubyVM::InstructionSequence.compile("puts 'hello'")
pp ObjectSpace.reachable_objects_from(iseq)
#=>
[RubyVM::InstructionSequence,
 #<InternalObject:0x1062c54 T_ARRAY>,
 "<compiled>",
 "<compiled>",
 #<InternalObject:0x1062c7c T_NODE>]

ここでは,4つのオブジェクトが取れました.そのうち2つが InternalObject になっています.残り2つの "<compiled>" という文字列は,ISeq#path とかで取るための文字列ですね.

では,隠された Array を示す #<InternalObject:0x1062c54 T_ARRAY> に,何が入っているか見てみましょう.

require 'objspace'
require 'pp'

iseq = RubyVM::InstructionSequence.compile("puts 'hello'")
hide_array = ObjectSpace.reachable_objects_from(iseq)[1]
pp ObjectSpace.reachable_objects_from(hide_array)
#=>
["hello"]

文字列リテラルとして利用した "hello" という文字列を格納しているということがわかりました.

そもそも,なぜこの配列を隠しオブジェクトにしているかというと,この配列を ObjectSpace.each_object などで取り出して,勝手に変更を加えると,このバイトコード列を実行するのに問題が生じるためです.で,この隠し配列オブジェクトがなんだったかというと,バイトコードが利用するオブジェクトを,バイトコードが生存しているあいだは mark するための配列です.具体的には rb_iseq_t::mark_ary です.

なお,InternalObjectWrapper に対して ObjectSpace.reachable_objects_from を行うと,その wrap した対象である内部オブジェクトが参照しているオブジェクト群を返します.この機能を加えることで,「あるオブジェクトを起点とした,オブジェクトの関係グラフ」が利用できることになります.

もう1つ加えておくと,今回は 1 番目にこの隠し配列オブジェクトがある,ということがわかりましたが,この順番はとくに保証されていません.具体的には,mark 処理の順番で格納されていくことになります.

さて,InternalObjectWrapper#object_id は,もちろんこの wrapper オブジェクトの object_id を取り出しますが,wrap したオブジェクトの object_id が取り出したいことがあります.それを,InternalObjectWrapper#internal_object_id によって得ることが出来ます.昨日の記事では,この機能を使って memsize_of_all_reachable_objects_from(obj) を実現していました.

では,今日はこの辺で.

_8(Sat)

Ruby VM アドベントカレンダーの 8 日目です.

そろそろ息切れしてきました.

今日は,

で紹介されている ObjectSpace.reachable_objects_from(obj) をネタにしてみます.なお,このメソッドは require 'objspace' として,objspace ライブラリを require しないと使えません.

ObjectSpace.reachable_objects_from(obj) は,obj から直接辿ることができるオブジェクトをすべて列挙して返します.

次に例を示します.

  • (1) When obj is ["a", "b", "c"], returns [Array, "a", "b", "c"]
  • (2) When obj is ["a", "a"], returns [Array, "a", "a"]
  • (3) When obj is [a = "a", a], returns [Array, "a"]

(1) は,問題無いと思いますが,(2),(3) の違いはわかるでしょうか.同じオブジェクトを複数回辿ることが出来るとき,1つしか返さないようになっています.

さて,これを使って,obj から辿ることができる,すべてのオブジェクトが必要とするメモリサイズを計算するコードを書いてみます.「直接たどることができる」ではなく,「辿ることができる全てのオブジェクト」というところに注意して下さい.つまり,再帰的に ObjectSpace.reachable_objects_from(obj) を適用しよう,というわけです.

例えば,[['a' * 100, 'b' * 100], 'c' * 100] という配列は,長さが 100 の文字列オブジェクト 3 つ辿れるので,合計 300 byte と出て欲しいわけです(厳密には,Array クラスオブジェクトも辿れるのですが,これは計算が面倒なので除外することにします).

あるオブジェクトが消費するメモリサイズを得るには,ObjectSpace.memsize_of(obj) というメソッドを利用します.これは,1.9 の時から入っています.

早速ですが,コードです.

require 'objspace'
require 'pp'

def memsize_of_all_reachable_objects_from(obj, exclude_class = Module)
  objs = {}
  queue = [obj]
  while obj = queue.pop
    next if objs[obj.object_id]
    next unless reachable_objects = ObjectSpace.reachable_objects_from(obj)
    reachable_objects.each{|o|
      case o
      when ObjectSpace::InternalObjectWrapper
        next if objs[o.internal_object_id]
      else
        next if objs[o.object_id]
      end
      queue.push o if !exclude_class || !o.kind_of?(exclude_class)
    }
    objs[obj.respond_to?(:internal_object_id) ? obj.internal_object_id : obj.object_id] = obj
  end
  objs.inject(0){|r, (_, o)| r += ObjectSpace.memsize_of(o)}
end

memsize_of_all_reachable_objects_from(obj, klass=Module) は,obj から辿れるすべてのオブジェクトのサイズを返します.ただし,第二引数 klass で指定されたクラスのオブジェクトは除外します.デフォルトでは Module クラスのオブジェクト,つまり Array などは辿らないようにしてみます.nil とすることで除外しないようになります.

では,やってみましょう.

p memsize_of_all_reachable_objects_from([['a' * 100, 'b' * 100], 'c' * 100])
#=> 300

ちゃんと 300 が帰ってきました.

せっかくなので,Array も含めてサイズを取ってみます.

p memsize_of_all_reachable_objects_from([['a' * 100, 'b' * 100], 'c' * 100], nil)
#=> 639428

えらい数が帰ってきました.Array クラスから辿れるオブジェクトは,凄い数にのぼり,その合計サイズも大きい,ということになります(途中にある objs のエントリ数を見てみるとわかるのですが,辿ることができるすべてのオブジェクトは 5604 個だそうです.ただし,環境による可能性があります).

memsize_of_all_reachable_objects_from() は,比較的簡単なメソッドですが,ObjectSpace::InternalObjectWrapper というものを使っていることに注意が必要です.これについては,また次の機会にご紹介します(としてネタをつなげる).興味がある方は ext/objspace/objspace.c を読んでみて下さい.

では,今日はこの辺で.


この話は RubyConf Taiwan 2012 のために書いていたコードでした.

資料:http://www.atdot.net/~ko1/activities/rubyconf.tw2012_ko1.pdf

資料には,図付きで説明があります.参考にして下さい.

_7(Fri)

Ruby VM アドベントカレンダーの 7 日目です.

昨日の話はたるいさんに修正をしてもらったので,そちらを注意して下さい.

なぜ,ブロックの中で起こる例外のクラスが Timeout::Error と違うかというと,複数の timeout をネストさせたときにちゃんとわかるため,だそうです.

というわけで,今日はここまでにしておいておきます(手抜き).


発表練習とかなかなか終わらない.

_6(Thu)

Ruby VM アドベントカレンダーの 6 日目です.

本当はメソッド呼び出しの高速化の話の続きを書こうと思っていたのですが,予定を変えて非同期イベントについての話を書こうと思います.

とりあえず書いておくと,Ruby 2.0 から timeout(3){loop{}} のようなプログラムが終わらなくなりそうです(詳細は以下).ご自分のプログラムで,このような例が無いか確認してみて下さい(実際に,ご自分のプログラムを最新の trunk でテストして頂くのが早いです).もし,そのような例が互換性についてとても問題,ということになるようだったら,この挙動は変更される可能性があります.


timeout(30) { ... } と書いておくと,30秒後たってもブロックを実行中の場合,ブロックは Timeout::Error 例外により中断されます.

require 'timeout'
timeout(30){
  sleep 40
}
#=> t.rb:3:in `sleep': execution expired (Timeout::Error)

これはどうやって実現しているかというと,timeout ブロックを実行する時にスレッドをもう1つたてて,そのスレッドが 30 秒後にブロック実行中のスレッドに Thread#raise を使って Timeout::Error メソッドをあげます.30 秒以内にブロックを終了した場合は,そのタイムアウトを投げてくるスレッドを停止することで例外を発生させないようにします.

さて原理は簡単なのですが,これには次のような問題がありました.

require 'timeout'
timeout(30){
  begin
    ... # 処理 X
  ensure
    ... # 後始末処理
  end
}

このようなプログラムが起きたとき,処理 X の後で,必ず後始末処理が走ることが期待されます.例えば,ファイルを消したりだとか.

しかし,処理 X 中に Timeout::Error が発生したのなら良いのですが,後始末処理の最中に Timeout::Error が発生するとまずいことになります.

このような問題に対処するために,Thread.async_interrupt_timing というものが導入されました(名前は変更される可能性があります.というか,多分変更します).

面倒くさい話は置いといて,この場合どうやって使うかというと,Thread.async_interrupt_timing(Timeout::Error => :defer) { ... } としておくと,このブロックの部分では Timeout::Error 例外が来ても無視(ブロックから抜けたときは普通に例外があがります)となります(ちなみに,:defer というシンボル名も,変わる可能性があります.新しい仕様なので,まだ名前で悩んでいるのです).

逆に,Thread.async_interrupt_timing(Timeout::Error => :immediate) と指定すると,すぐに例外が上がることになります.

これを使って,後始末の最中に例外処理が起きないようにすると,次のように書きます.

require 'timeout'
Thread.async_interrupt_timing(Timeout::Error => :defer){
  # ここでは一切 Timeout::Error は受け取らない(中断しない)
  timeout(30){
    begin
      Thread.async_interrupt_timing(Timeout::Error => :immediate){ 
        ... # 処理 X.ここでは Timeout::Error によって中断される
      }
    ensure
      ... # 後始末処理.ここでは一切 Timeout::Error は受け取らない(中断しない)
    end
  }
}

こう書けば,後始末処理の中で Timeout::Error で中断されることがないため,安全に後始末を書くことが出来ます.

ちなみに,

require 'timeout'
timeout(30){
  begin
      ... # 処理 X.ここでは Timeout::Error によって中断される
    }
  ensure
    Thread.async_interrupt_timing(Timeout::Error => :defer){ 
      ... # 後始末処理.ここでは一切 Timeout::Error は受け取らない(中断しない)
    }
  end
}

こっちのほうが簡単でいいじゃないか,と言われるかもしれませんが,これだと ensure が始まった瞬間に Timeout::Error を受信して後始末処理がおこなわれなくなる,という危険があります.ちなみに,現在の MRI の実装では,このタイミングでは非同期イベントが起こりえないので,その心配はないのですが,一般的には気を付けるべきでしょう.

さて,毎回 timeout を書くごとにこのように冗長な記述をするのは面倒なので,みんな書かなくなる → 変なところで Timeout::Error が起こるようなプログラムになってしまう,という懸念があります.

そこで,[Bug #7503] make timeout.rb async-interrupt safe by default において,timeout の挙動を変えてしまおうという変更が小崎さんによってされています(これ,バグじゃないよなぁ).

先ほど,Thread.async_interrupt_timing では :never(例外をあげない) と :immediate(例外をすぐに上げる),という指定をしましたが,もう一つ,:on_blocking という指定があります(しつこいようですが,:on_blocking という名前は変更される可能性があります).

この :on_blocking という指定をすると,指定された例外を,主に I/O 処理まで遅延することが出来ます.外部からの例外によって中断させたい利用例の多くは I/O の中断なので,これで事足りる,という見方が出来ます.

[Bug #7503] の提案では,「timeout で中断できるブロックを,:on_blocking で囲んでおいて,中断は I/O 処理などのタイミングでしか出来ないようにしよう」というものになっています.このようにすることで,ensure での後始末などの間に割り込まれることを防ぐことが出来ます.

しかし,これには互換性に問題があり,例えば I/O 処理の含まない計算を timeout でデフォルトでは中断できないようになっています.

require 'timeout'
timeout(30){
  # 時間のかかる計算(I/O無し)
]

もしくは,無限ループを中断出来なくなります.

require 'timeout'
timeout(30{
  loop{} # 無限ループ
}

まぁ,後者をまともに書く人は居ないとは思いますが,前者はあり得るんじゃないの,という気がしています(なので,互換性100%という目標を掲げている Ruby 2.0 に入れて良いのか私はあんまり乗り気じゃないのです).

このように,従来通り即座に中断して欲しいときは,timeout(sec, immediate: true) のように呼べば良い,ということであり,書き直せばちゃんと動きます.「この書き直してね」といって許容できるかどうか,がこの修正の互換性的なキモになります.

というわけで,今回は次の3つのご紹介でした.

  • timeout には危険がいっぱい
  • Thread.async_interrupt_timing で安全に
  • timeout を変更して安全に
    • 変更は非互換だけど,許容範囲内?(という私からの質問)

では今日はこの辺で.


初羽田空港国際線ターミナルで初台湾.この歳でもどんどん初めてのことが出来るってのはありがたいことだなぁ.いや,やったことがないことばかりなんだけどさ.

_tarui(Thu Dec 06 10:30:07 +0900 2012)

コメントのpreviewにだまされたので、もう一度投稿してみる。

多分、以下のようにする必要がー ensureでなんとかするようにしたいですね。 preview2では

# -*- coding: utf-8 -*-
require 'timeout'
e=Class.new(Exception)
Thread.async_interrupt_timing(e => :defer){
  # ここでは一切 Timeout::Error は受け取らない(中断しない)
  begin
    timeout(30,e){
      begin
        Thread.async_interrupt_timing(e => :immediate){
          ... # 処理 X.ここでは timeout によって中断される
        }
      ensure
        ... # 後始末処理.ここでは一切 timeout は受け取らない(中断しない)
      end
    }
    Thread.async_interrupt_timing(e=>:immediate){}
  rescue e
  end
}

trunk(バグってるかもしれない)では

# -*- coding: utf-8 -*-
require 'timeout'
e=Class.new(Exception)
timeout(30,e){
  begin
    Thread.async_interrupt_timing(e => :immediate){
      ... # 処理 X.ここでは timeout によって中断される
    }
  ensure
    Thread.async_interrupt_timing(e => :defer){     #blockingな処理をやる場合に必要
      ... # 後始末処理.ここでは一切 timeout は受け取らない(中断しない)
    }
  end
}

または

# -*- coding: utf-8 -*-
require 'timeout'
e=Class.new(Exception)
timeout(30,e){
  # preview2ではここでtimeoutによって中断される可能性がある
  Thread.async_interrupt_timing(e => :defer){     #previe2 or ensureでblockingな処理をやる場合に必要
    begin
      Thread.async_interrupt_timing(e => :immediate){
        ... # 処理 X.ここでは timeout によって中断される
      }
    ensure
      ... # 後始末処理.ここでは一切 timeout は受け取らない(中断しない)
    end
    }
  }
}

一番最後のコードはpreview2でもだいたい上手くうごきます。(つまりコメントで書いた場所を除いて)

_tarui(Thu Dec 06 11:49:28 +0900 2012)

 理由を書くのを忘れてました。つまり、timeoutのブロック内で発生する例外はTimeout::Errorではないからです。

_5(Wed)

Ruby VM アドベントカレンダーの 5 日目です.

バックトレースの話が続いたので,ちょっと別の話をします.

今年の RubyConf 2012 で,メソッドの高速化の話をしました. Ruby はメソッド呼び出しを沢山行うので,メソッドの高速化は大事です.

Ruby 2.0 では,send メソッドの最適化を色々と行っています.発表では,それらの中のエッセンスを紹介しました.発表資料は http://www.atdot.net/~ko1/activities/rubyconf2012_ko1_pub.pdf にあるので,参考にしてみて下さい.

今日はこの辺で(手抜き).


RSS の生成タイミングが深夜 0 時(JST)なので,なんかさぼってるように見られてそうだな.

_4(Tue)

Ruby VM アドベントカレンダーの 4 日目です.

これまで,Ruby 2.0 から導入される caller まわりの話をご紹介してきました.今日はバックトレースまわりの最後の話です.多分.

Ruby では,例外発生時にバックトレース情報が保存されます.保存しておかないと,あとで(例えばスタックを巻き戻した後で)その情報を参照できないためです.

Ruby 1.9 以前では,例外発生時に caller が返すようなバックトレース情報(文字列の配列)を生成し,例外オブジェクトが保持しているようになっていました.しかし,これはちょっと遅いです.フレームサイズ分,文字列オブジェクトを生成して,配列に格納しなければなりませんから.

そこで,Ruby 2.0 では,例外発生時には Thread::Backtrace というオブジェクトを生成することにしました.これは,Exception オブジェクトからは直接は参照できない特殊なオブジェクトになっています(ObjectSpace.each_objects で頑張れば参照できます).

Exception#backtrace が呼ばれたとき,Thread::Backtrace オブジェクトを文字列配列にする処理が走り,従来の(caller が返すような)文字列配列を初めて作成します.このようにすることで,Exception#backtrace を参照しない場合(制御フローとして例外を利用しているような場合)のオーバヘッドを削減しています.

では,実際に測ってみましょう.

require 'benchmark'

def rec n=100, &b
  n == 0 ? yield : rec(n-1, &b)
end

max = 100_000

Benchmark.bm{|x|
  x.report(" 10"){
    max.times{
      begin
        rec(10){raise}
      rescue # ignore raise
      end
    }
  }
  x.report("100"){
    max.times{
      begin
        rec(100){raise}
      rescue # ignore raise
      end
    }
  }
}
__END__
ruby 1.9.3p332 (2012-11-15 revision 37660) [i386-mswin32_100]
             user     system      total        real
 10 rec  2.262000   0.031000   2.293000 (  2.352799)
100 rec 10.437000   0.141000  10.578000 ( 10.638851)

ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
            user     system      total        real
 10 rec 0.640000   0.015000   0.655000 (  0.652583)
100 rec 2.199000   0.031000   2.230000 (  2.280790)

全スタックフレームの,バックトーレス情報を生成するために必要になる情報 (*1) を保存をするので,どうしてもスタックトレースのサイズに比例した時間がかかることになってしまいますが,しかし 1.9 に比べて随分と速くなっていることがわかるのではないでしょうか.

(*1) 具体的には iseq + pc になります.本当はもうちょっと複雑ですが.

(例外的にしか発生されない例外の生成が速くなって,嬉しい人は限定的だとは思いますが...)

なお,caller および caller_locations は,この Thread::Backtrace オブジェクトを1度生成してから,それをベースに文字列の配列,もしくは Thread::Backtrace::Location の配列を生成します.

というように,Ruby 2.0 からバックトレースまわりの構造ががらっと変わりました.詳しく知りたい人は vm_backtrace.c を読んでみて下さい.

では,この辺で.


三日坊主が回避されたので,そろそろ力尽きるかもしれない.

_3(Mon)

Ruby VM アドベントカレンダーの 3 日目です.

今日も,Ruby 2.0 の新機能のご紹介です.

昨日 ご紹介した caller_locations ですが,これを使うことで簡単にパス名などだけを取り出すことができます.つまり,caller で必要であったパス名の切り出しのような正規表現の呼び出しなどが不要になります.そうすると,つまり,性能が良くなるのです.

caller をプロファイラなどに利用しようとするとき,性能はとても大事になります(性能は大事.ここ,テストに出ます).

では,実験してみましょう.正規表現で切り出す版と,path メソッドだけで取り出す版を作り,時間を計ってみました.

require 'benchmark'

max = 100_000

Benchmark.bm{|x|
  x.report{
    max.times{
      /(.+):\d/ =~ caller(0)[0]
    }
  }
  x.report{
    max.times{
      caller_locations(0)[0].path
    }
  }
}
#=>
ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
       user     system      total        real
   1.872000   0.000000   1.872000 (  1.882239)
   0.312000   0.000000   0.312000 (  0.315540)

path メソッドで取り出す版は,6 倍程度速いことがわかります.まぁ,そりゃそうですよね.

(なお,ここではちょうテキトーな正規表現を使っているため,":" を含むようなパス名は正しくパースできません.その辺も,path メソッドを使うメリットですね)

さて,性能を考えると,そもそも「1つ上のフレームしか要らんのなら,1つ上だけ返してくれればいいのに」ということになります.

Ruby 2.0 から,Kernel#caller および Kernel#caller_locations は第2引数 n を取ることができ,これはスタックの深い場所から n 個取り出す,という意味になります.やってみましょう.

def m
  p caller(0)    #=> ["t.rb:2:in `m'", "t.rb:7:in `<main>'"]
  p caller(0, 1) #=> ["t.rb:3:in `m'"]
  p caller(0, 2) #=> ["t.rb:4:in `m'", "t.rb:7:in `<main>'"]
end

m

無引数だと,全部取り出す,1 と指定すれば 1 個だけ取り出す,ということがわかったと思います.

では,これが性能にどれくらい影響するか見てみましょう.

require 'benchmark'

max = 100_000
Benchmark.bm{|x|
  x.report("caller w/o 2nd arg"){
    max.times{
      caller(0)[0]
    }
  }
  x.report("caller w/ 2nd arg"){
    max.times{
      caller(0, 1)[0]
    }
  }
  x.report("caller_loc w/o 2nd arg"){
    max.times{
      caller_locations(0)[0]
    }
  }
  x.report("caller_loc w/ 2nd arg"){
    max.times{
      caller_locations(0, 1)[0]
    }
  }
}

#=>
ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
                            user     system      total        real
caller w/o 2nd arg      1.513000   0.000000   1.513000 (  1.555697)
caller w/ 2nd arg       0.265000   0.000000   0.265000 (  0.259033)
caller_loc w/o 2nd arg  0.297000   0.000000   0.297000 (  0.298538)
caller_loc w/ 2nd arg   0.093000   0.000000   0.093000 (  0.090512)

第2引数で指定したものが随分と速いことがわかります.

なお,これはスタックフレームのサイズが大きいとより顕著になります(ちなみに,このときのスタックフレームのサイズは 9 でした).

ためしに,100個ほど足して(つまり,109フレーム)で測ってみましょう.

require 'benchmark'

def rec n=100, &b
  n == 0 ? yield : rec(n-1, &b)
end

max = 100_000
Benchmark.bm{|x|
  x.report("caller w/o 2nd arg"){
    rec{
      max.times{
        caller(0)[0]
      }
    }
  }
  x.report("caller w/ 2nd arg"){
    rec{
      max.times{
        caller(0, 1)[0]
      }
    }
  }
  x.report("caller_loc w/o 2nd arg"){
    rec{
      max.times{
        caller_locations(0)[0]
      }
    }
  }
  x.report("caller_loc w/ 2nd arg"){
    rec{
      max.times{
        caller_locations(0, 1)[0]
      }
    }
  }
}

#=>
ruby 2.0.0dev (2012-12-01 trunk 38127) [i386-mswin32_100]
                            user     system      total        real
caller w/o 2nd arg     19.172000   0.188000  19.360000 ( 19.644995)
caller w/ 2nd arg       0.500000   0.015000   0.515000 (  0.530067)
caller_loc w/o 2nd arg  3.478000   0.000000   3.478000 (  3.494444)
caller_loc w/ 2nd arg   0.297000   0.031000   0.328000 (  0.335042)

なんと,約40倍も違います.Raila などのフレームワークを利用すると,スタックフレームが大きくなることが多いので,もし caller の1フレーム分だけが欲しい,という時は活用してください.

さて,Rails という例が出てきたので,今日の最後の話題を紹介します.

Rails などのフレームワークの場合,スタックの深いところのフレームはフレームワークの分であり,興味のあるアプリケーションのスタックフレームは浅いところの一部だけ,ということがあります.つまり,caller の部分配列を取り出したい,という用途です.

そこで,caller および caller_locations は第一引数に Range を受けるようになりました.

caller(2..8) とすると,2つ上から8個上までの6フレーム分の情報を取り出します.また,フレームワークのスタックフレームがn個消費していることがわかっていれば,caller(1..-n) とすることで,1つ上のフレームから,全スタックフレームのサイズ - n の部分スタックフレームを取り出すことができます.

ちょっとわかりづらいですが,Array#[range] と全く同じように作ってありますので,困ったら Array#[range] の使い方を見て下さい.

ちなみに,今日紹介した第2引数の追加,および第1引数に Range を受け取れるようにする拡張は,Thread#backtrace および Thread#backtrace_locations にも行われています.

では,今日はこの辺で.


caller 関係しかでてこねーじゃねーか!

でも,もうちょっとだけ続くのじゃ.


http://gihyo.jp/news/info/2012/12/0101 に補足されなくて,やはり勘違いしているようで,何よりです.これからも勘違いしていきたい.

_2(Sun)

Ruby VM アドベントカレンダーの 2 日目です.

今日も,Ruby 2.0 の新機能のご紹介です.

これまで,現在の実行箇所のバックトレース情報を取得するための Kernel#caller というメソッドがありましたが,これを若干使いやすくする Kernel#caller_locations というメソッドが導入されました.

バックトレース情報とは,実行中のスレッドが,どんなメソッド呼び出し(およびブロック呼び出し)を行っているか,というものを示す情報で,例外発生時に出力されるようなものです.

require 'pp'

def m1
  raise
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

pp m5
#=>
t.rb:4:in `m1': unhandled exception
	from t.rb:7:in `m2'
	from t.rb:8:in `m3'
	from t.rb:9:in `m4'
	from t.rb:10:in `m5'
	from t.rb:12:in `<main>'

ちなみに,例外発生時のバックトレース情報は,Exception#backtrace によって取得することができます.

require 'pp'

def m1
  raise
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

begin
  pp m5
rescue => e
  pp e.backtrace
end

["t.rb:4:in `m1'",
 "t.rb:7:in `m2'",
 "t.rb:8:in `m3'",
 "t.rb:9:in `m4'",
 "t.rb:10:in `m5'",
 "t.rb:13:in `<main>'"]

さて,現在のバックトレースを取得するたびに毎回例外を出していては大変なので,Kernel#caller メソッドが存在します.

require 'pp'

def m1
  caller
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

pp m5
#=>
["t.rb:7:in `m2'",
 "t.rb:8:in `m3'",
 "t.rb:9:in `m4'",
 "t.rb:10:in `m5'",
 "t.rb:12:in `<main>'"]

Exception#backtrace で取り出したバックトレース情報にくらべて,`m1' メソッド実行中である,という情報が落ちていますね.caller メソッドは第一引数に整数値を取り,何個目のバックトレースを取得するのか,という指定が行え,デフォルト値は 1 になっています.つまり,1 つ上のトレースを取得する,ということになります.これを 0 にすると,現在のフレーム位置のトレースを取得することができます.

require 'pp'

def m1
  caller(0)
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

pp m5
#=>
["t.rb:4:in `m1'",
 "t.rb:7:in `m2'",
 "t.rb:8:in `m3'",
 "t.rb:9:in `m4'",
 "t.rb:10:in `m5'",
 "t.rb:12:in `<main>'"]

これで,Exception#backtrace と同じ表示になりました.なお,なぜデフォルト値が 1 であるか,ですが,そういうユースケースが多かったから,ですかねぇ.

ちなみに,他のスレッドの現在のバックトレース情報は何か,ということを調べる Thread#backtrace というメソッドが存在します.

require 'pp'

def m1
  sleep
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

th = Thread.new{
  m5
}
sleep 0.1
pp th.backtrace
#=>
["t.rb:4:in `sleep'",
 "t.rb:4:in `m1'",
 "t.rb:7:in `m2'",
 "t.rb:8:in `m3'",
 "t.rb:9:in `m4'",
 "t.rb:10:in `m5'",
 "t.rb:13:in `block in <main>'"]

Thraed#backtrace メソッドを利用することで,スレッド th は,今 sleep で寝ている,ということがわかりますね.


さて,1.9 までの caller では,上記のように "#{path}:#{lineno} #{method_name}" のような形の文字列の配列が返ってきます.ただ,時々 path だけ,lineno だけ,method_name だけが欲しい,という場合があります.この場合,正規表現なんかで文字列を切り出さなければなりません.

そこで,Ruby 2.0 から Kernel#caller_locations というメソッドが導入される予定です(多分).caller と同じ使い方で使えるのですが,返ってくる値が Thread::Backtrace::Location というクラスのオブジェクトになります.ちょっと長いですが,ふつーの人は,このクラス名を書くことは無いので許して下さい.

では,caller の代わりに caller_locations を使ってみます.

require 'pp'

def m1
  caller_locations
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

pp m5
#=>
["t.rb:7:in `m2'",
 "t.rb:8:in `m3'",
 "t.rb:9:in `m4'",
 "t.rb:10:in `m5'",
 "t.rb:12:in `<main>'"]

はい,さっきと同じ結果が得られましたね.同じような表示になってしまうのでちょっとわかりづらいので,各要素のクラスを確認してみましょう.

require 'pp'

def m1
  caller_locations
end

def m2; m1; end
def m3; m2; end
def m4; m3; end
def m5; m4; end

pp m5.map{|e| e.class}
#=>
[Thread::Backtrace::Location,
 Thread::Backtrace::Location,
 Thread::Backtrace::Location,
 Thread::Backtrace::Location,
 Thread::Backtrace::Location]

Thread::Backtrace::Location というクラスであることがわかります.Thread::Backtrace::Location.inspect が,意図的に String#inspect の結果と同じ表示をするよになっているから,同じように見えるんですね.

(え,あなたの Ruby 2.0 では

[#<Thread::Backtrace::Location:0x0000000159e920>,
 #<Thread::Backtrace::Location:0x0000000159e8f8>,
 #<Thread::Backtrace::Location:0x0000000159e8d0>,
 #<Thread::Backtrace::Location:0x0000000159e8a8>,
 #<Thread::Backtrace::Location:0x0000000159e880>]

と表示されるって? もしかして,まだ Ruby 2.0.0 preview2 をお使いじゃないんですか!? ([ANN] ruby 2.0.0-preview2 released))

(inspect の変更を preview2 リリース直前に入れたのでした)

さて,Thread::Backtrace::Location はいくつかの便利なメソッドを持っています.まず,to_s を行うと,caller の返す文字列と同じ表記になります.

他には,次のようなメソッドがあります.

  • path: パス名を返します.
  • absolute_path: フルパスを(あれば)返します.
  • lineno: 行番号を返します.
  • label: メソッド名などを返します.
  • base_label: メソッド名などを返します.ブロックなどの情報は入りません.

label と base_label に関しては説明が必要でしょう.

例えば,ブロックの中で caller を呼び出すと,次のように表示されます.

1.times{
  p caller(0)
}
#=> ["t.rb:3:in `block in <main>'", "t.rb:2:in `times'", "t.rb:2:in `<main>'"]

トップレベルなので,<main> がメソッド名にあたる部分でですが,ブロックの中で実行されているので,`block in <main>' という表記が書いてあります.ただ,ここで欲しいのは <main> だけだったりすると,これもわざわざ正規表現で切り出すのはしんどいので,base_label とすれば <main> の部分だけ取り出せる,というようにしています.

では,実際に試してみましょう.

require 'pp'

def m1
  1.times{
    $locs = caller_locations
  }
end

def m2; m1; end

class C
  tap{
    m2
  }
end

$locs.each{|loc|
  puts "path         : #{loc.path}"
  puts "absolute_path: #{loc.absolute_path}"
  puts "lineno       : #{loc.lineno}"
  puts "label        : #{loc.label}"
  puts "base_label   : #{loc.base_label}"
}

#=>
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 4
label        : times
base_label   : times
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 4
label        : m1
base_label   : m1
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 9
label        : m2
base_label   : m2
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 13
label        : block in <class:C>
base_label   : <class:C>
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 14
label        : tap
base_label   : tap
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 14
label        : <class:C>
base_label   : <class:C>
path         : t.rb
absolute_path: c:/ko1/src/rb/t.rb
lineno       : 11
label        : <main>
base_label   : <main>

昨日 紹介した ISeq#path なんかと同じようなインターフェースになっているというのがわかると思います.というか,同じようになるように作りました.

なお,Thread#backtrace も,Thread::Backtrace::Location の配列を返す Thread#backtrace_locations というメソッドが追加されています.

では,今日はこんな感じで.


というわけで,Ruby 2.0.0 preview2 が出ました!([ANN] ruby 2.0.0-preview2 released))

皆さん試して下さい.協力して下さった全ての皆様に感謝致します.

_kou(Mon Dec 03 10:10:13 +0900 2012)

inspectの結果は


[#<Thread::Backtrace::Location "t.rb", 7, "in `m2'">,
 ...]

とか、


[#<["t.rb", 7, "in `m2'"]>,
 ...]

というようにパースされた結果が分かれていたほうがうれしいなぁと思いました。caller_locationsを使いたいときはパースされた結果が欲しいので、どうパースされたかがわかったほうが嬉しいです。 to_sの方はcallerと同じでよいと思います。

_ささだ(Mon Dec 03 10:49:40 +0900 2012)

 ML でやりましょう.ticket 作って貰えますか?

_1(Sat)

Ruby VM advent calendar #1

もう 12 月ですね.早いものです. アドベントカレンダーというのが流行っているそうなので,ローテク(手書き)でなんか書いてみます.ネタは Ruby VM に関係する何かで埋めてみます.


VM といえば bytecode ですが,Ruby の VM でバイトコードを表現するのが RubyVM::InstructionSequence クラスのオブジェクトです.長いので ISeq とします.

さっき,preview2 に間に合わせるために急いで RubyVM::InstructionSequence.of(obj) というメソッドを作りました.何をするかというと,Proc か Method を受け取って,ISeq を返すメソッドです.

それから,ISeq#path などを作りました.ISeq がどのファイルにあったか,というのを返します.ほかにも variant がありますが,まぁ見ればわかりますよね.

これがあると,こんなテストが書けます.

  LINE_OF_HERE = __LINE__
  def test_location
    iseq = ISeq.of(method(:test_location))

    assert_equal(__FILE__, iseq.path)
    assert(/#{__FILE__}/ =~ iseq.absolute_path)
    assert_equal("test_location", iseq.label)
    assert_equal("test_location", iseq.base_label)
    assert_equal(LINE_OF_HERE+1, iseq.first_lineno)

    line = __LINE__
    iseq = ISeq.of(Proc.new{})
    assert_equal(__FILE__, iseq.path)
    assert(/#{__FILE__}/ =~ iseq.absolute_path)
    assert_equal("test_location", iseq.base_label)
    assert_equal("block in test_location", iseq.label)
    assert_equal(line+1, iseq.first_lineno)
  end

それから,次のようなコードで,現在存在する ISeq をすべて集めることができます.最終的に,ファイル名ごとに集計し,行数でソートした形で出力しています.

require 'pp'

h = Hash.new{|h, k| h[k] = []}
ObjectSpace.each_object(RubyVM::InstructionSequence){|iseq|
  h[iseq.absolute_path || iseq.path] << iseq
}

h.each{|file, iseqs|
  puts file
  pp iseqs.map{|iseq| [iseq.first_lineno, iseq.label]}.sort
}

今日はこの辺で.


多分盛大にアドベントカレンダーを勘違いしているんだろうな.

Sasada Koichi / ko1 at atdot dot net
$Date: 2003/04/28 10:27:51 $