Google V8 JavaScript EngineでのWebAssemblyのi32.addの実装を見てみる

WebAssembly(以下,wasm)については,既に多くの解説記事が存在するため, wasmについての説明は割愛する. ここでは,wasmにおいてとある命令i32.addがどのように実行されるのかを見ていく. 参照する実装は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はスタックマシンとなっている.ここでは,これまでで出てきた関数 PopPushからスタックの実装をみていく.

  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;
  }

PopPush,いずれも実装はたったの1行だけであることがわかる.

以上,Google V8 JavaScript Engineでのwasmの実装を簡単に見ていった. 正直なところ,最適化などで非常に複雑かつ高度な実装になっていると思っていたが, 今回読んだ部分はナイーブな実装となっており,読みやすいといえる. 一方で,JavaScriptとのインターフェースとなる部分は相当複雑であることが 容易に想像できる.

参考文献