Diary

Diary?

学生の研究日記だったらしいです。多分。

開発日記。

オススメの本(頂いた本):

いちばんあたらしいの2016 6/28 15:18

_28(Tue)

Proc を WB protected にしようとしている。といっても、実は Proc じゃなくて、Proc から参照される Env(ローカル変数のストレージ)を WB protected にしようとしている。

Env と ENV は違うので注意。RubyVM::Env という隠しオブジェクト。 ちなみに、こいつは 2.4 では見えなくする予定。

そうすると、

a = Object.new

みたいな取るに足らないコードも、実は Env への書き込みの可能性があるので、

env[:a] = Object.new

のように扱わねばならない。

具体的には、MRI では、Proc が作られると、その Proc オブジェクトが参照可能なローカル変数を Env オブジェクトに追い出す。こんな感じ。

def test
  a = 1 # まだ、スタック上にいる
  Proc.new{
    p a
  } # この時点で、変数 a の寿命はメソッド test の寿命より長いんで、
    # a は Env (e としよう)にコピーされる
  a = Object.new # e[:a] = a のような挙動になるので、
                 # write barrier が必要
end

こんな感じ。

ローカル変数アクセスをするごとに、「これはスタック上か? スタック上じゃなかったら Env へのライトバリアが要るよね?」ってコードになるので、ローカル変数のアクセスが遅くなる。

# vm1_lvar_set_object0
def test
  i = 0
  obj = Object.new
  a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = nil

  while i<30_000_000 # while loop 1
    i += 1
    a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = obj
  end
end

test

こんなコードを、trunk と今回 hack した結果と mruby の実行時間(秒)を並べておく。空の whileloop の実行時間は消してあるので、純粋に代入だけと考えれば良い。

                         trunk   modified        mruby
vm1_lvar_set_object0*    1.961      2.004        2.622

trunk と modified では、若干遅い。 mruby は、それらより遅い。

このケースでは、test メソッドの Env は生成していなかったので、スタックに書き込むだけ、となっている。なので、「ローカル変数はスタック上にあるのかな?」というチェックのオーバヘッドで遅くなっている。


次は、Env が生成されるパターン。

# vm1_lvar_set_object1
def test
  i = 0
  obj = Object.new
  a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = nil
  Proc.new{}
  while i<30_000_000 # while loop 1
    i += 1
    a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = obj
  end
end

test

途中の Proc.new{} した時点で、test のローカル変数は Env に待避される。 常識的に考えれば、Proc.new{} からは、test のローカル変数を参照してないんだから、 別に待避させなくてもいいんじゃね、という気分になるけど、Proc#binding という機能があるので、a とかにアクセス出来ちゃう。(see also lambda lifting)

また、さらに言うと、常識的に考えれば Proc.new{} は誰も参照していないんだから、 そもそも要らねーだろ、という気はするが、世の中には TracePoint というものがあって、返値を capture することで、escape が出来るという... なんというか、色々つらい話があります。

そこまでやるのは病的だろう、という感じもしますが(それについては同意します)、現状ではこうなっている。まぁ、実際のところ、色々例外ケースを考えるのがしんどいので現状の、とにかく無差別に作る、というようなことをしている。

さて、結果。

                        trunk   modified        mruby
vm1_lvar_set_object0*   1.961      2.004        2.622
vm1_lvar_set_object1*   1.996      3.313        2.567

予想通り、modified が、遅くなっている。 「ローカル変数は Env 上にいるか? お、要るな。つまり、Env -> obj への参照が生成されたぞ、つまり、write barrier が必要だ」となって、WB のオーバヘッドがかかっている。「嘘、私の WB 重すぎ?」ってな感じです。

代入、1.5 倍くらい遅くなってますね。いいかなぁ、これくらいなら。どうなんだろ。代入、どれくらいします?

(口調が変わる)

面白いのは、mruby は速度がほとんど変わっていないことです。これは何故かというと、mruby はメソッドから抜ける、ローカル変数を Env(相当)に待避させません。メソッドからから抜ける(この場合は、test メソッドが終わる)タイミングで、はじめてローカル変数の領域を確保します。

この方法の良い点はいくつかありますが、(1) 見てわかるように WB が不要になる (2) スタック領域を使い続けるので、メモリ効率が若干よくなる、といったものがあると思います。

欠点は、リターン時に待避するかどうかのチェックが必要になります(see also return barrier)。また、MRI の現在の構造だと、その環境を参照している Proc が参照しているポインタを、リターン時にすべて書き換える必要があります、が、これは Env を必ず経由するようにすれば解決するからどうでもいいな。

いや、MRI でもそれくらいやってもいいんじゃね、という気はしますが、 本当に動くかちょっと不安。メリットは、多少のメモリ効率の向上でしょうか。


さて、mruby は WB が不要かというと、そうでもないです。

# vm1_lvar_set_object2
def test
  i = 0
  obj = Object.new
  a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = nil

  1.times{
    while i<30_000_000 # while loop 1
      i += 1
      a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = obj
    end
  }
end

test

ここでは、ブロックの中から、外の変数に代入しています。

結果。

                        trunk   modified        mruby
vm1_lvar_set_object0*   1.961      2.004        2.622
vm1_lvar_set_object1*   1.996      3.313        2.567
vm1_lvar_set_object2*   1.948      2.019        9.595

modified は遅くなっていませんが、mruby が遅くなっています。

MRI では、1.times ... のようなことをしても、Proc(Env)を作りません。なので、Proc 生成を陽に行なわない、ブロック呼び出しが速いです(ここでは関係ないですが)。 しかし、mruby では、Proc/Env を生成することになります。

そして、ブロックの上の方のローカル変数へ代入するときは、スタック上にあろうと無かろうと、とにかく WB をすることになっています。

def foo
  a = 1
  Proc.new{ a = Object.new }
end

foo.call

こんなケースを考えて貰うとわかるのですが、a = Object.new としたタイミングでは、メソッド foo のフレームは存在しないため、VM のスタックを参照しても、生成されたオブジェクトへ辿り着くことができません。もし、Env が old だった場合、生成したオブジェクトはマークされなくなってしまいます。そのため、mruby では、WB をやっているんだと思います。


が、まだスタック上にあるんだから、今回の場合は、別に WB 要らないよね。

というわけで、スタック上にある場合は、WB しないようにしてみました。

index 6f8c510..2c15c48
--- a/src/vm.c
+++ b/src/vm.c
@@ -972,7 +972,9 @@ RETRY_TRY_BLOCK:
         mrb_value *regs_a = regs + GETARG_A(i);
         int idx = GETARG_B(i);
         e->stack[idx] = *regs_a;
-        mrb_write_barrier(mrb, (struct RBasic*)e);
+
+       if (!MRB_ENV_STACK_SHARED_P(e))
+         mrb_write_barrier(mrb, (struct RBasic*)e);
       }
       NEXT;
     }

1行加えただけ。

                        trunk   modified        mruby         mruby.modified
vm1_lvar_set_object0*   1.961      2.004        2.622
vm1_lvar_set_object1*   1.996      3.313        2.567
vm1_lvar_set_object2*   1.948      2.019        9.595
vm1_lvar_set_object2*   1.966      2.014        10.122           7.387

あれ、あんまり速くなんないですね...。環境辿るのが遅いのかな。 まぁ、ちょっと改善しました。


MRI も、別にローカル変数の代入するたびに、毎回 Env 取り出して WB する、みたいなことをしなくても、

  • コントロールフレームスタックから参照される Env については、VM(スレッド)のマーク時に mark する
  • leave 時(メソッドが返るとき)、ep が Env を参照してたらその Env をリメンバーセットに入れておく

ということで対処可能です。どーしよっかなぁ。 ちょっと、工夫は必要になるが。


おまけ。 vm1_lvar_set_object1 では、Env は若いままなんで、WB やっても、「若いオブジェクト→...」という参照では、WB は最終的に無視されます。

そこで、Env を古くしてみましょう。

# vm1_lvar_set_object3
def test
  i = 0
  obj = Object.new
  a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = nil
  Proc.new{}
  GC.start; GC.start; GC.start # make env old

  while i<30_000_000 # while loop 1
    i += 1
    a = b = c = d = e = f = g = h = j = k = l = m = n = o = p = q = r = obj
  end
end

test
name                    trunk   modified        mruby   mruby-modified
vm1_lvar_set_object0*   1.962      1.944        2.599            2.686
vm1_lvar_set_object1*   1.944      3.241        2.595            2.646
vm1_lvar_set_object3*   1.974      4.001        2.600            2.679

ちゃんと、WB 追加で modified が遅くなっているのがわかります。 しかし、WB が不要なのに、その判断のために遅くなっているやつがつらいですね。色々理由はあるんですが、ここを解決できればいいんだけど、incremental marking 中であることがわかればなぁ。


今回の結果のまとめ。

name                    trunk   modified        mruby   mruby-modified
loop_whileloop          0.579      0.596        1.471            1.492
vm1_lvar_set_object0*   1.962      1.944        2.599            2.686
vm1_lvar_set_object1*   1.944      3.241        2.595            2.646
vm1_lvar_set_object2*   1.965      2.006        9.578            7.415
vm1_lvar_set_object3*   1.974      4.001        2.600            2.679

MRI は loop 速いね!


最近、昼の予定なんてなかったので油断していたら、久々にやらかしてしまった。

ご迷惑をおかけして大変申し訳ありません。

Log

2002 01 02 03 04 05 06 07 08 09 10 11 12
2003 01 02 03 04 05 06 07 08 09 10 11 12
2004 01 02 03 04 05 06 07 08 09 10 11 12
2005 01 02 03 04 05 06 07 08 09 10 11 12
2006 01 02 03 04 05 06 07 08 09 10 11 12
2007 01 02 03 04 05 06 07 08 09 10 11 12
2008 01 02 03 04 05 06 07 08 09 10 11 12
2009 01 02 03 04 05 06 07 08 09 10 11 12
2010 01 02 03 04 05 06 07 08 09 10 11 12
2011 01 02 03 04 05 06 07 08 09 10 11 12
2012 01 02 03 04 05 06 07 08 09 10 11 12
2013 01 02 03 04 05 06 07 08 09 10 11 12
2014 01 02 03 04 05 06 07 08 09 10 11 12
2015 01 02 03 04 05 06 07 08 09 10 11 12
2016 01 02 03 04 05 06 07 08 09 10 11 12

SASADA Koichi (ko1 at atdot dot net) / Skype ID: ko1_ssd


rss