昨日の続き。
色々整理して、
trunk modified vm1_lvar_set_object0* 1.937 2.280 vm1_lvar_set_object1* 1.995 2.081 vm1_lvar_set_object2* 2.025 2.230 vm1_lvar_set_object3* 1.978 2.461
こんな感じに。まぁ、これくらいなら良いかなぁ。
ep の需要が高まってきているので、ep をレジスタにするべきか否か。
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 する、みたいなことをしなくても、
ということで対処可能です。どーしよっかなぁ。 ちょっと、工夫は必要になるが。
おまけ。 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 速いね!
最近、昼の予定なんてなかったので油断していたら、久々にやらかしてしまった。
ご迷惑をおかけして大変申し訳ありません。
そういえば、先日 laptop を新調するか凄い悩んで、結局先送りすることにした。
モチベーション
Let's note を10年以上使い続けている(もうそんなになるんか)ので、今度は SX5 かと思って調べてみても、
ThinkPad で20万くらいのが、Let's note で40万、みたいな価格設定で、どうにも悩んでしまって、結局、現状のまま、もう少し待つか、というところに。
で、この Let's note NX2 をもう少し使い続けるために、ウェブでてきとうに検索して、次のような強化を行なった。
AC に対応してなかったので、家の AP はてきとうな奴だったけど、せっかく AC 対応になるので、良い奴を買った。
ネットワークむっちゃ速くなった。快適。色々、ネットワークの構成もシンプルに(Wifi の中継器が不要になったり)。
しかし、夜中の4時くらいに、update が走ったのか、急にネットワークが切れたつらい。
NX2 には、バッテリーが軽い奴と重い奴があって、軽い奴をずっと使っていたら、すぐに寿命が来てしまったので、最近はずっと重い奴を使い続けている。重いだけに、10時間くらいもつので便利。軽い奴は30分程度くらいしかもたないくらいだった。のだが、軽いのは、やはり軽い(重い奴に比べて 200g くらい軽い)ので、新しく発注した。
のだけど、メーカー在庫無し、と言う連絡がきてしまった。もう少し頑張ってくれ Panasonic。
slack team を2つ見ていたくなったのだけど、ウェブ経由だと、2つページを開いていないといけないぽいので、しょうがないから slack for windows をインストール。一体何個メッセージアプリがあるんだ。ほんとつらい。
つらい。
そういえば、先日、東京11の支払いを、だいたい終わらせた。 予算規模が600万とか、ちょっと個人が扱う額じゃないよね...。 税務署に相談に行ったりして大変でした。
今回の経験は、今度どっかにまとめたい。
たまっていた日記を 5/2 からずっと付けてた。 付けてたって言うか、とにかく埋めたというか...。
最近、えらい暑いね。
近所の図書館にラノベが大量に置いてあって、読みまくってるんだけど、何をどこまで読んだかすぐ忘れる(デュラララ!! ってどこまで読んだんだっけ)。
なにか記録するものを使った方がいいような気もするが、はて。