サイボウズ・ラボユース最終成果報告会
@Constellation
Yusuke Suzuki
結果, test262の, デファクト挙動に依拠したtestをあぶり出すことができる.
1 function FixedArray(num) {
2 var ary = new Array(num);
3 Object.defineProperty(ary, 'length', { writable: false });
4 return ary;
5 }
6
7 var fixed = FixedArray(10);
8 fixed.push(10); // 例外が起こるべき
1 function test() {
2 var i = 0, obj;
3 try {
4 i = (obj = {
5 flag: false,
6 valueOf: function() {
7 this.flag = true;
8 throw new Error;
9 }
10 }) += 10;
11 } catch (e) {
12 return i === 0 && obj.flag;
13 }
14 }
15 test();
その後Register VMに
compilerはframeの大きさを解析可能なので, 関数呼び出し前にVM stackが溢れないか調査できる
1 function test() {
2 test();
3 }
4 test(); // RangeError: maximum call stack size exceeded
frameは以下のようなlayout
ARG2 | ARG1 | THIS | FRAME.... | LOCAL REGISTERS | HEAP REGISTERS | TEMP ...
^ ^
fp register start
argumentsはregister最右端に逆順につまれる. JSはargumentsの数があっていなくてもいいので.
これにより, 先のframeから, registerの負数の番地, 例えば, r-7などでargumentsをregisterとして扱うことができる. (register windowのslide) この-の値には, frame structのsizeをregisterのsizeで割ったものが利用され, これはsizeof周りでlv5のcompile時に分かる.
frameには現在の環境や, 返り値を入れるregisterの番号, 戻り先pcなど制御情報が入っている. alignmentをあわせてallocateされているため, JSValの配列であるVM stack上に直接確保可能.
local変数で, 別の関数から参照されないものは, register上に直接置かれる. (local registers)
残りは計算用のtemporary registers.
example
BINARY_ADD
opcode | dst | lhs | rhs
LOAD_CONST
opcode | dst | offset
IF_FALSE
opcode | jmp | cond
Stack VMにおいて, ある場所にあるopcodeがpushする先のindexはframeの底から数えて常に同じである.
同じでないとすれば, あるpathを通ってここまできた場合と, 違う場合でstackに残る値の数が異なるという事態が起こる. そのようなことはありえない(あったらbug)
example
1 function test(a, b) {
2 return a + b;
3 }
Stack VM bytecode
1 [code] depth: 4 local: 2 heap: 0
2 00000: LOAD_PARAM 0 // ここでは常に[0]に値をpush
3 00002: STORE_LOCAL 0
4 00004: POP_TOP // ここでは常に[0]の値をpop
5 00005: LOAD_PARAM 1 // ここでは常に[0]に値をpush
6 00007: STORE_LOCAL 1
7 00009: POP_TOP // ここでは常に[0]の値をpop
8 00010: LOAD_LOCAL 0 // ここでは常に[0]に値をpush
9 00012: LOAD_LOCAL 1 // ここでは常に[1]に値をpush
10 00014: BINARY_ADD // ここでは常に[0]と[1]で計算, [0]に値をpush
11 00015: RETURN // 個々では常に[0]をreturn, pop
ならば, これをそのままregisterの値として置き換えれば, (初期的な)Register VMが完成
Register VM bytecode
1 [code] depth: 4 local: 2 heap: 0
2 00000: LOAD_PARAM r0 0
3 00002: STORE_LOCAL r0 0
4 00005: LOAD_PARAM r0 1
5 00007: STORE_LOCAL r0 1
6 00010: LOAD_LOCAL r0 0 // local変数[0]をr0に
7 00012: LOAD_LOCAL r1 1 // local変数[1]をr1に
8 00014: BINARY_ADD r0 r0 r1
9 00015: RETURN r0
ここで, local変数はStack VMでもstack上におかれているので, register VMでもregisterに置くことができる. 先ほどのLOAD_LOCAL / STORE_LOCALをMVに書き換える. local変数は2つなので, stack部分, つまりtemporaryなregisterは2からはじめる.
Register VM bytecode + MV
1 [code] depth: 4 local: 2 heap: 0
2 00000: LOAD_PARAM r2 0
3 00002: MV r0 r2 // もとSTORE_LOCAL
4 00005: LOAD_PARAM r2 1
5 00007: MV r1 r2 // もとSTORE_LOCAL
6 00010: MV r2 r0 // もとLOAD_LOCAL, local変数r0をr2に
7 00012: MV r3 r1 // もとLOAD_LOCAL local変数r1をr3に
8 00014: BINARY_ADD r2 r2 r3
9 00015: RETURN r2
で, これを見ると, 明らかに無駄なMV...
今, Register VMはlocal変数をregisterで表し, かつregisterを演算に引き取れるので,
Register VM bytecode + use local
1 [code] depth: 4 local: 2 heap: 0
2 00000: LOAD_PARAM r0 0 // local r0に直接
3 00005: LOAD_PARAM r1 1 // local r1に直接
4 00014: BINARY_ADD r2 r0 r1 // 演算結果をr2のtemporary registerに
5 00015: RETURN r2
JavaScriptは,
このため, 素直にStack VMのように評価した順に積んだ場合, 呼び出し側でstackにいくつ積まれたかによって, 呼び出され側から見た引数の場所が変わってしまうので, compile時に判断できない.
ところが, Stack VMではなくなったRegister VMでは, 評価した値を別のoffsetに明示的に積むことができる. つまり, 評価の逆順に積んでいくことができる.
[arg3][arg2][arg1][arg0][this][frame...]
すると, frame側から見て, arg0は常に-1の場所にあることになる. これにより, argumentsをそのままregisterとして用いることが可能.
問題はargumentsが少なかった時, つまりcallee側は3引数あると思ってr-3まで使っているのに2つしか積んでいなかった時
この場合は, frameを作る時に数を数えて, 足りなければ, 横にずらすことでうまくいく.
Register VM bytecode + arguments opt
1 [code] local: 0 heap: 0 registers: 1
2 000000: BINARY_ADD r0 r-10 r-11
3 000002: RETURN r0
現行railgun::Compilerのemitするcode
今, 以下のscript
1 function test() {
2 var a = 10;
3 return a + (a = 20);
4 }
を考える. この時, aをr0として, 単純においてしまうと, こうなるのではないか?
1 LOAD_INT32 r0 10 // var a = 10;
2 LOAD_INT32 r0 20 // (a = 20)
3 BINARY_ADD r1 r0 r0 // a + (a = 20) ? えっ!
4 RETURN r1
問題は, Stack VMの場合はstackに積んでいた, copyしていたので, いわば評価済みであったのに対して, Register VMがlocal変数のregisterをそのまま使ってしまうことにより, 評価されず, ここがあとで書き変わってしまう自体が起こっている.
RHSでの副作用に注意しなければいけない.
Compilerの定石としては, ここでやるのではなく, 後で複写を削除する.
しかし, VMのBytecode Compilerは高速にBytecodeを出力しないといけない.
1 passで, 複雑なデータ構造(Graph)を作らずに, が望ましい.
そもそもそのような文法は許諾していない. また, 未定義とする.
1 function p()
2 local x = 0
3 function inner()
4 x = 20
5 return 1000
6 end
7 print(x + inner())
8 end
9 p() -- 1020
JSならば, 1000となるべき
JSCのとっている方法. RHSにregisterに対して副作用を起こすものがあるときには, 値をMVで退避させれば良い.
いま, a + (a = 20)について, a = 20が副作用があると判断されたので, aをMVする.
1 LOAD_INT32 r0 10 // var a = 10;
2 MV r1 r0 // 退避!
3 LOAD_INT32 r0 20 // (a = 20)
4 BINARY_ADD r1 r1 r0
5 RETURN r1
このやり方でうまくいく.
もちろん, 副作用がRHSにあるかどうかは, parserが事前に計算しておけばいいが, labelごとにセットを作るわけにもいかないので, 副作用があるなし程度の情報になってしまう.
1 function test() {
2 var a = 10;
3 var b = 30;
4 return b + (a = 20);
5 }
が
1 LOAD_INT32 r0 10 // var a = 10;
2 LOAD_INT32 r1 10 // var b = 30;
3 MV r2 r1 // いらないけれど退避...
4 LOAD_INT32 r0 20 // (a = 20)
5 BINARY_ADD r2 r2 r0
6 RETURN r2
(a = 20)
が副作用ありと判断された結果, MVせざるを得なくなっている.
もちろん, (a = 20)が変数aに対する副作用であるということを記録することが出来ればいいのだが, それをするとexprの各nodeに副作用セットを格納する必要があり, 利用頻度に対してコストが高すぎる.
lv5の実装. コンビニにラムレーズンアイスを買いに行った時に思いついた方法を実装.
そもそも, これが問題になるのは何かというと, Stack VMのときはいちいちStackにcopyしていた, ある種評価していたのに, Register VMでは評価しなくなったのが問題.
今, 評価されていないけれど後で使う変数をmap上にlistとして保管しておき, その変数に対して副作用が起こるbytecodeがemitされそうになったら退避させる.
以前話した際には, CoW的という評価を.
不勉強で申し訳ないのですが, 何か名前が付いているのであれば教えていただければありがたいです.
1 function test() {
2 var a = 10;
3 var b = 30;
4 return b + (a = 20);
5 }
が
1 LOAD_INT32 r0 10 // var a = 10;
2 LOAD_INT32 r1 30 // var b = 30;
3
4 // map[r1]にr1をつないでおく.
5 LOAD_INT32 r0 20 // r1には影響なし
6 BINARY_ADD r2 r1 r0
7 RETURN r2
一方,
1 function test() {
2 var a = 10;
3 return a + (a = 20);
4 }
が
1 LOAD_INT32 r0 10 // var a = 10;
2
3 // まず, aが評価され, これはregister直接使えることが分かる
4 // map[r0]にr0をつないでおく.
5 // ここで r0へのLOAD_INT32をemitしそうになる.
6 // map[r0]が空でないので, これを急遽MV
7 MV r1, r0
8 LOAD_INT32 r0 20
9 BINARY_ADD r1 r1 r0
10 RETURN r1
となり, 本当に書き換わる時のみregisterを退避させる, 効率的なcodeが1 passで生成できる.
ただ, これを利用すると, いきなりMVが入る可能性があるという問題点がある. 一部の場所では利用できないので, railgun::Compilerは上の2つの両方を用いている.
example:
SunSpider benchmark result (spent time. smaller is faster)
V8 Suite
https://gist.github.com/2200277
みなさま
ありがとうございました
むしろRegister VMのほうが小さくなった. なぜか?
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |