mrubyのattr系メソッドの速度
mrubyのbenchmarkディレクトリにao-render.rb(AOベンチ)があるわけだが、このコードを見ると例えばVevtorクラスのgetter/setterは自前で定義されている。作った人(@miura1729さん)によると元はytl用のものでattr系メソッドが無かったからということなのだが、これをattr系で定義したら速くなるのだろうか。
Ruby1.9.3では速くなる。
require 'benchmark' class Foo def hoge @hoge end def hoge=(v) @hoge = v end end class Bar attr_accessor :fuga end Benchmark.bm do |bm| bm.report("Foo#hoge") do a = Foo.new 1000000.times do a.hoge = a.hoge end end bm.report("Bar#fuga") do b = Bar.new 1000000.times do b.fuga = b.fuga end end end #=> user system total real # Foo#hoge 0.235000 0.000000 0.235000 ( 0.234375) # Bar#fuga 0.187000 0.000000 0.187000 ( 0.187500)
が、mrubyではとても遅くなる。
class Foo def hoge @hoge end def hoge=(v) @hoge = v end end class Bar attr_accessor :fuga end from = Time.new a = Foo.new 1000000.times do a.hoge = a.hoge end p Time.new - from from = Time.new b = Bar.new 1000000.times do b.fuga = b.fuga end p Time.new - from #=>0.59375 # 2.453125
なぜかと言うと、getter/setterがself.instance_variable_get/setを呼ぶRubyコードのメソッドとして定義されるからで、mrblibの中に含まれているからである。単純にインスタンス変数を参照する固定コードと比べると当然遅い。evalがあればそれで定義して同等の速度にまで持っていけるのだろうが、それでも同等にしかならない。
これをCで書いたら速くなるだろうか。試してみた。とりあえずreaderだけ。
static mrb_value mrb_attr_reader_func(mrb_state *mrb, mrb_value self) { mrb_sym sym = mrb->ci->mid; int len; const char *name; name = mrb_sym2name_len(mrb, sym, &len); { char buf[len + 2]; buf[0] = '@'; memcpy(buf + 1, name, len + 1); return mrb_iv_get(mrb, self, mrb_intern2(mrb, buf, len + 1)); } return mrb_nil_value(); } static mrb_value mrb_mod_attr_reader(mrb_state *mrb, mrb_value mod) { mrb_value obj; const char *name; int len; mrb_get_args(mrb, "o", &obj); if (mrb_type(obj) != MRB_TT_SYMBOL) { mrb_value str = mrb_funcall(mrb, obj, "inspect", 0); mrb_raise(mrb, E_TYPE_ERROR, "%s is not a symbol", mrb_string_value_ptr(mrb, str)); } if (!(name = mrb_sym2name_len(mrb, mrb_symbol(obj), &len))) { return mrb_nil_value(); } mrb_define_method(mrb, mrb_class_ptr(mod), name, mrb_attr_reader_func, ARGS_NONE()); return mrb_nil_value(); }
0.59375 2.9375
前より遅くなった。
mrubyではCで定義されたメソッドの呼び出しは非常に速い。Procオブジェクトとして存在して、シンプルに関節分岐でVMから直接呼び出す仕掛けになっている。OP_ENTERもOP_RETURNもいらない。引数が無ければスタックを参照することも無い。
それがなぜ遅いのかというと、恐らくメソッド名のシンボルから文字列を取り出し、@をつけてシンボル化→インスタンス変数アクセスというハッシュ計算しまくりな構造になってしまうからではないかと思う。これをうまいことカットできれば速くのなるのかもしれないが、とりあえずいい手は思い浮かばないので諦め。