遠藤です。

現在の Enumerator#each は、1 回のブロック呼び出しで 2 回分の yield を実行
しています。

  1. Enumerator#each が元のメソッドを rb_block_call で呼ぶ
     (このときブロックとして enumerator_each_i を与える)
  2. 元のメソッドが yield して現在のブロック (enumerator_each_i) を呼ぶ
  3. enumerator_each_i が yield して実際のブロックを呼ぶ


そこで rb_block_call に現在のブロックを渡せるようにしてみました。
こうすると、yield は 1 回で済みます。

  1. Enumerator#each が元のメソッドを rb_block_call で呼ぶ
     (このときブロックとして現在のブロックをそのまま渡す)
  2. 元のメソッドが yield して実際のブロックを呼ぶ


以下のベンチマークで試すと、パッチ前で 1.49 秒、パッチ後で 1.00 秒と
なりました。

  GC.disable
  a = [0] * 5000000
  t = Time.now
  a.to_enum.each {|x| }
  p Time.now - t

ちなみに a.to_enum.each の代わりに a.each と直接呼んだ場合でも 1.00 秒
です。メソッドをベースとした Enumerator が to_enum しない場合と遜色ない
速度になったと思います。


この話は Enumerator に特化した話ではなく、ARGF.each_line や each_byte
なども同じように 2 段階 yield になっていたので、現在のブロックを直接
渡すようにしたところ、以下が 0.20 秒から 0.18 秒に高速化しました。

  ruby -e '$<.each_line {|l| }' *.c


具体的なパッチの内容は、rb_block_call の bl_proc に NULL が渡されたら
現在のブロックを使うようにするというものです。
コミットしてもいいでしょうか。

diff --git a/enumerator.c b/enumerator.c
index 7c50f3d..34d16c0 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -330,12 +330,6 @@ enumerator_allocate(VALUE klass)
 }

 static VALUE
-enumerator_each_i(VALUE v, VALUE enum_obj, int argc, VALUE *argv)
-{
-    return rb_yield_values2(argc, argv);
-}
-
-static VALUE
 enumerator_init(VALUE enum_obj, VALUE obj, VALUE meth, int argc, VALUE *argv)
 {
     struct enumerator *ptr;
@@ -473,7 +467,7 @@ static VALUE
 enumerator_each(VALUE obj)
 {
     if (!rb_block_given_p()) return obj;
-    return enumerator_block_call(obj, enumerator_each_i, obj);
+    return enumerator_block_call(obj, 0, obj);
 }

 static VALUE
diff --git a/io.c b/io.c
index 643455e..3b8bd25 100644
--- a/io.c
+++ b/io.c
@@ -9148,7 +9148,7 @@ argf_each_line(int argc, VALUE *argv, VALUE argf)
     RETURN_ENUMERATOR(argf, argc, argv);
     for (;;) {
 	if (!next_argv()) return argf;
-	rb_block_call(ARGF.current_file, rb_intern("each_line"), argc, argv,
rb_yield, 0);
+	rb_block_call(ARGF.current_file, rb_intern("each_line"), argc, argv, 0, 0);
 	ARGF.next_p = 1;
     }
 }
@@ -9182,7 +9182,7 @@ argf_each_byte(VALUE argf)
     RETURN_ENUMERATOR(argf, 0, 0);
     for (;;) {
 	if (!next_argv()) return argf;
-	rb_block_call(ARGF.current_file, rb_intern("each_byte"), 0, 0, rb_yield, 0);
+	rb_block_call(ARGF.current_file, rb_intern("each_byte"), 0, 0, 0, 0);
 	ARGF.next_p = 1;
     }
 }
@@ -9212,7 +9212,7 @@ argf_each_char(VALUE argf)
     RETURN_ENUMERATOR(argf, 0, 0);
     for (;;) {
 	if (!next_argv()) return argf;
-	rb_block_call(ARGF.current_file, rb_intern("each_char"), 0, 0, rb_yield, 0);
+	rb_block_call(ARGF.current_file, rb_intern("each_char"), 0, 0, 0, 0);
 	ARGF.next_p = 1;
     }
 }
diff --git a/vm_eval.c b/vm_eval.c
index f0b2316..9276f0a 100644
--- a/vm_eval.c
+++ b/vm_eval.c
@@ -828,9 +828,15 @@ rb_iterate(VALUE (* it_proc) (VALUE), VALUE data1,
     if (state == 0) {
       iter_retry:
 	{
-	    rb_block_t *blockptr = RUBY_VM_GET_BLOCK_PTR_IN_CFP(th->cfp);
-	    blockptr->iseq = (void *)node;
-	    blockptr->proc = 0;
+	    rb_block_t *blockptr;
+	    if (bl_proc) {
+		blockptr = RUBY_VM_GET_BLOCK_PTR_IN_CFP(th->cfp);
+		blockptr->iseq = (void *)node;
+		blockptr->proc = 0;
+	    }
+	    else {
+		blockptr = GC_GUARDED_PTR_REF(th->cfp->lfp[0]);
+	    }
 	    th->passed_block = blockptr;
 	}
 	retval = (*it_proc) (data1);

-- 
Yusuke ENDOH <mame / tsg.ne.jp>