Google V8 JavaScript EngineでのWebAssemblyのi32.addの実装を見てみる
WebAssembly(以下,wasm)については,既に多くの解説記事が存在するため,
wasmについての説明は割愛する.
ここでは,wasmがどのように実装され,実行されるのかを見ていく.
参照する実装はGoogle V8 JavaScript Engineの
1b254a25163fd84a7696ff62e87cb6dcde7e13f2
である.
簡単なコード例
次のようなwasmのコードを考える.
(module
(func (param $lhs i32) (param $rhs i32) (result i32)
get_local $lhs
get_local $rhs
i32.add
)
)
このコードは2つのパラメータを加算して返す関数を表している. 具体的には,パラメータ$lhsと$rhsをスタックにプッシュし, i32.addによりそれらの加算を行なっている.
i32.addの実装からたどる
最初にi32.addはどのように実装されているのかを見ていく.
src/wasm/wasm-interpreter.cc
には次のような定義がある.
#define FOREACH_SIMPLE_BINOP(V) \
V(I32Add, uint32_t, +) \
I32Add
とあるので,ここがi32.addの定義であることがわかる.
次にFOREACH_SIMPLE_BINOP
が使われている部分を見る.
#define EXECUTE_SIMPLE_BINOP(name, ctype, op) \
case kExpr##name: { \
WasmValue rval = Pop(); \
WasmValue lval = Pop(); \
auto result = lval.to<ctype>() op rval.to<ctype>(); \
possible_nondeterminism_ |= has_nondeterminism(result); \
Push(WasmValue(result)); \
break; \
}
FOREACH_SIMPLE_BINOP(EXECUTE_SIMPLE_BINOP)
EXECUTE_SIMPLE_BINOP
を引数としてFOREACH_SIMPLE_BINOP
を
呼び出していることがわかる.
ここでは,何らかのcase文となっておりその中で,
スタックから値をポップしrvalとlvalに代入し,与えられたopで計算を行い,
resultに代入し,値をプッシュしていることがわかる.
つまり,ここで実際の計算が行われていることがわかる.
では,このcase文の大元を見てみる.
switch (orig) {
case kExprNop:
break;
ここから,origにより判定していることがわかる. origは
byte orig = code->start[pc];
であり,codeは
void Execute(InterpreterCode* code, pc_t pc, int max) {
からExecute
の引数であることがわかる.
さらに,Execute
は
WasmInterpreter::State Run(int num_steps = -1) {
DCHECK(state_ == WasmInterpreter::STOPPED ||
state_ == WasmInterpreter::PAUSED);
DCHECK(num_steps == -1 || num_steps > 0);
if (num_steps == -1) {
TRACE(" => Run()\n");
} else if (num_steps == 1) {
TRACE(" => Step()\n");
} else {
TRACE(" => Run(%d)\n", num_steps);
}
state_ = WasmInterpreter::RUNNING;
Execute(frames_.back().code, frames_.back().pc, num_steps);
// If state_ is STOPPED, the current activation must be fully unwound.
DCHECK_IMPLIES(state_ == WasmInterpreter::STOPPED,
current_activation().fp == frames_.size());
return state_;
}
よりWasmInterpreter::State Run
から呼び出されている.
コードを追うのは一旦このあたりでやめておく.
スタックの実装
wasmはスタックマシンとなっている.ここでは,これまでで出てきた関数
Pop
やPush
からスタックの実装をみていく.
WasmValue Pop() {
DCHECK_GT(frames_.size(), 0);
DCHECK_GT(StackHeight(), frames_.back().llimit()); // can't pop into locals
return *--sp_;
}
void Push(WasmValue val) {
DCHECK_NE(kWasmStmt, val.type());
DCHECK_LE(1, stack_limit_ - sp_);
*sp_++ = val;
}
Pop
,Push
,いずれも実装はたったの1行だけであることがわかる.
以上,Google V8 JavaScript Engineでのwasmの実装を簡単に見ていった. 正直なところ,最適化などで非常に複雑かつ高度な実装になっていると思っていたが, 今回読んだ部分はナイーブな実装となっており,読みやすいといえる. 一方で,JavaScriptとのインターフェースとなる部分は相当複雑であることが 容易に想像できる.