読者です 読者をやめる 読者になる 読者になる

abcdefGets

ゲッツ!

V8 Iginition Interpreter

以前、東京Node学園25時限目で発表した内容を修正して書いていこうと思う。

というわけで、V8にバイトコードインタープリタ Ignition が搭載された。
このインタープリタは単純そうに見えて非常にわかりづらいので解説していく。

バイトコードインタープリタとは

インタープリタとは、ソースコードを逐次実行する形式のエンジン。
今までのV8はソースコードを即アセンブラコンパイルし実行していたが、
インタープリタはそれとは違い、一度ソースコードを高レベルなバイト命令に変換し、そのバイト命令を逐次実行していく。
高級アセンブラみたいな感じ。

Ignition概要

Ignitionはレジスタベースのバイトコードインタープリタである。Javaのスタックベースとは違って、CPUのレジスタに実際に値を割り付けて実行する。

IgnitionはBytecodeHandlerと呼ばれるバイトコード処理関数を予め生成しておき、バイトコードから配列のインデックスを取得、
そのインデックスに生成された処理関数を割り当て、Bytecodeの配列を次々巡回して、対応するインデックスの関数を呼び出しコードを実行する。

JSで非常に単純化されたコードを書くと以下の様になる。

var Bytecodes = [0,1,2,3,4,5];
var index = 0;
function dispatch(next) {BytecodeHandlers[next]();}
const BytecodeHandlers = {
  ['0']() {...; dispatch(Bytecodes[index++])},
  ['1']() {...; dispatch(Bytecodes[index++])},
  ['2']() {...; dispatch(Bytecodes[index++])},
  ['3']() {...; dispatch(Bytecodes[index++])},
  ['4']() {...; dispatch(Bytecodes[index++])},
  ['5']() {...; dispatch(Bytecodes[index++])},
}

このモデルを念頭にV8のコードベースを確認していく。

Ignitionの構造

バイトコード生成までの道のり

IgnitionはJavascript ASTからバイトコードを生成する。
このバイトコード生成のステップを確認していく。

BytecodeGeneratorAstVisitorを実装しているので、Javascript ASTを巡回しながら対応しているバイトコードを生成していく。 BytecodeGeneratorsrc/interpreter/bytecode-generator.hにあり、バイトコード生成メソッドはBytecodeGenerator::GenerateBytecodeである。

さて、BytecodeGenerator::GenerateBytecodeはどこから呼ばれるかというと、InterpreterCompilationJob::ExecuteJobImpl(src/interpreter/interpreter.cc)内で呼び出される。
InterpreterCompilationJob::ExecuteJobImplstatic Interpreter::NewCompilationJobで実行される。

Interpreter::NewCompilationJobの階層は以下のようになっている。

Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

このstatic Interpreter::NewCompilationJobコンパイラパイプラインのジョブを生成するメソッドなので、compiler.cc(src/compiler.cc)を見ていこう。

compiler.cc(src/compiler.cc)は非常に複雑でわかりづらい呼び出し階層をもっており、さらにオプションの設定パーサーの設定も相まって非常に読みづらい。
static Interpreter::NewCompilationJobを呼び出すまでのコールスタックは以下の様になっている。

ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
Interpreter::NewCompilationJob
  |
  FullCodeGenerator::NewCompilationJob

ScriptCompiler::CompileがV8のJavascript Compilerのエントリーポイントとなっており、そこから順次関数を呼び出し、最終的にInterpreterのJobを生成する。

最終的なBytecodeGenerator::GenerateBytecodeまでの呼び出しコールスタックは以下のようになる。

ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
| FullCodeGenerator::NewCompilationJob
|
Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

バイトコード生成

さて、呼び出し階層を把握したところで、バイトコードの生成方法を見ていく。
バイトコード生成は先程も書いたとおりAstVisitorを継承しているので、各種Visit***メソッドを実装する必要がある。
ので、各種Visit***の実装を見ていけば何をしているか理解できるはず。

ただ、闇雲にコードを見てもバイトコード自体は理解できないので、一旦trace_bytecodeでd8を実行してみる。

javascript

var a = 1;

bytecodes

0  [generating bytecode for function: ]
1  Parameter count 1
2  Frame size 32
3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4           0x3f5e20aafdf8 @    2 : 1f f9             Star r1
5           0x3f5e20aafdfa @    4 : 02                LdaZero
6           0x3f5e20aafdfb @    5 : 1f f8             Star r2
7           0x3f5e20aafdfd @    7 : 20 fe f7          Mov <closure>, r3
8           0x3f5e20aafe00 @   10 : 55 aa 01 f9 03    CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9      0 E> 0x3f5e20aafe05 @   15 : 92                StackCheck
10   116 S> 0x3f5e20aafe06 @   16 : 09 01             LdaConstant [1]
11          0x3f5e20aafe08 @   18 : 1f f9             Star r1
12          0x3f5e20aafe0a @   20 : 02                LdaZero
13          0x3f5e20aafe0b @   21 : 1f f8             Star r2
14          0x3f5e20aafe0d @   23 : 03 01             LdaSmi [1]
15          0x3f5e20aafe0f @   25 : 1f f7             Star r3
16          0x3f5e20aafe11 @   27 : 55 ab 01 f9 03    CallRuntime [InitializeVarGlobal], r1-r3
17          0x3f5e20aafe16 @   32 : 04                LdaUndefined
18   118 S> 0x3f5e20aafe17 @   33 : 96                Return
19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21  - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22  - length: 2
23            0: 0x3f5e20aafd71 <FixedArray[4]>
24            1: 0x2315b1a87ef9 <String[1]: a>

そうするとこのような結果が得られる。

さてバイトコードを出力したのはいいが、見方がわからないと意味が無いので、見方も解説。

ここは関数のbytecodeの場合に関数名が入る。今回はグローバルなので空。

0  [generating bytecode for function: ]

これはstackのパラメータの数。
今回のバイトコードはグローバルなので無視。

1  Parameter count 1

FrameSizeは割り当てたレジスタの数 * ポインタのサイズ
ポインタのサイズは大体の環境で、32bitでは4byte、64bitでは8byteになる。
今回の場合、割り当てたレジスタ数の数が4 64bit環境なので、ポインタサイズが8byte
4 * 8 = 32となる。

2  Frame size 32

各バイト列は 現在のアドレス アドレスのオフセット バイトコードの数値 バイトコードの名前 オペランド となっている。

3  0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]

ここは定数値プールの中身。
今回は変数名のaがプールされている。

19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21  - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22  - length: 2
23            0: 0x3f5e20aafd71 <FixedArray[4]>
24            1: 0x2315b1a87ef9 <String[1]: a>

さてこれらの情報を踏まえて、先程のソースコードバイトコードを見ていこう。

以下の部分はすっとばしてよい。ここはインタープリタの準備なので。

3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4           0x3f5e20aafdf8 @    2 : 1f f9             Star r1
5           0x3f5e20aafdfa @    4 : 02                LdaZero
6           0x3f5e20aafdfb @    5 : 1f f8             Star r2
7           0x3f5e20aafdfd @    7 : 20 fe f7          Mov <closure>, r3
8           0x3f5e20aafe00 @   10 : 55 aa 01 f9 03    CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9      0 E> 0x3f5e20aafe05 @   15 : 92                StackCheck

本番はここから
解説はコード中に書いていく。

            // 定数プールのインデックス1(変数名a)から値をaccumulatorにロードする。
10   116 S> 0x3f5e20aafe06 @   16 : 09 01             LdaConstant [1]
11          // accumulator(変数名a)からr1レジスタに値をロードする。
12          0x3f5e20aafe08 @   18 : 1f f9             Star r1
13          // accumulatorに0をロードする。
14          0x3f5e20aafe0a @   20 : 02                LdaZero
15          // accumulator(0)からr2レジスタに値をロードする。
16          0x3f5e20aafe0b @   21 : 1f f8             Star r2
17          // accumulatorに即値1をロードする。
18          0x3f5e20aafe0d @   23 : 03 01             LdaSmi [1]
19          // accumulator(1)からr3レジスタに値をロードする。
20          0x3f5e20aafe0f @   25 : 1f f7             Star r3
21          // r1レジスタからr3レジスタの値(a, 0, 1)を使ってInitializeVarGlobalランタイムを呼び出す。
22          0x3f5e20aafe11 @   27 : 55 ab 01 f9 03    CallRuntime [InitializeVarGlobal], r1-r3
23          // accumulatorにundefinedをセット
24          0x3f5e20aafe16 @   32 : 04                LdaUndefined
25          // 終了
26   118 S> 0x3f5e20aafe17 @   33 : 96                Return

これがバイトコードの実行である。
ちなみにCallRuntimeの場合、各Runtime毎に呼び出し規約が決まっているので、それぞれに合わせたレジスタの割り当てが必要になる。
InitializeVarGlobalランタイム呼び出しは以下のレジスタを期待している。

  • r0 = 束縛される変数名
  • r1 = LaunguageMode SLOPPY(通常) STRICT(strictモード) LAUNGUAGE_END(不明)
  • r2 = 束縛される値

そのため、上記のコードは

  • accumulatorに値をロード
  • レジスタに値をロード

を繰り返して、Runtime呼び出しのコードを生成している。

とこの調子でIgnitionはバイトコードを実行していくが、
そのバイトコードを実行しているのはBytecodeHandlerとよばれるクラスである。

バイトコード実行

BytecodeHandler

バイトコードの実行はBytecodeHandlerによって行われる。
このBytecodeHandlerはV8の初期化時に生成され、配置される。

以下がBytecodeHandlerの例である。

IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
  Node* zero_value = NumberConstant(0.0);
  SetAccumulator(zero_value);
  Dispatch();
}

LadZeroの処理を行うBytecoeHandlerで、中では単純にaccumulatorに0をセットするだけ。
このような調子で各バイトコードにつき一つのBytecodeHandlerが実装されている。

BytecodeHandlerは直接次のBytecodeHandlerを呼び出す。

このDispatchが次のBytecodeHandlerを呼び出している。

Dispatch();

しかし、このBytecodeHandlerの実装をみるとわかるのだが、BytecodeHandlerはあくまで、
実行予定Nodeを組み立てているだけで、実際には何かを実行するわけではない。

Ignitionインタープリタは最初にBytecodeの処理手順をグラフノードで生成し、生成したグラフからマシンコードを生成する。
これをBytecodeのdispatch-tableに設定することで、各バイトコード毎に行う処理が設定されたBytecodeHandlerが実装される。

以下の図はBytecodeHandlerの生成

f:id:brn_take:20170501171341p:plain

InterpreterEntryTrampoline

Ignitionは最終的にBytecodeArrayを生成し終わった後に、
InterpreterEntryTrampolineというbuiltinsからIgnitionのDispatchTableを発火するコード生成し、
BytecodeArrayからバイトコードを取り出し、対応するDispatchTableの処理を実行して回っていく。

以下の図はIgnitionが実行される様子

f:id:brn_take:20170501171955p:plain

まとめ

一通りIgnitionの実行パスを眺めた。
また、Ignitionのバイトコードアセンブラコードのキーとして振る舞い、
実際にはベースラインで生成されたコードが実行されている事を確認した。

TurboFan経由の最適化部分等については今後の記事を書く予定。