Binaryen を使用した Wasm のコンパイルと最適化

Binaryen はコンパイラおよびツールチェーンです。 インフラストラクチャ ライブラリです。その目的は、 直感的かつ高速かつ効果的に WebAssembly にコンパイルできます。この記事では、 ExampleScript という合成玩具言語の例で、 Binaryen.js API を使用した JavaScript の WebAssembly モジュール。このコースでは、 モジュールの作成、モジュールへの関数の追加、エクスポートの基本 説明します。これにより、Terraform ワークフローの 実際のプログラミング言語を WebAssembly にコンパイルする仕組み。さらに、 Binaryen.js と wasm-opt を指定します。

Binaryen の背景

Binaryen は C API 単一のヘッダーで記述でき、 使用されます。 入力を受け付けます。 WebAssembly フォーム 一般的なトピックや 制御フローのグラフ コンパイラが優先される傾向があります。

中間表現(IR)は、使用されるデータ構造またはコードである 内部的にはコンパイラや仮想マシンによってソースコードを表現されます。Binaryen の 内部 IR はコンパクトなデータ構造を使用し、完全に並列処理できるよう設計されている CPU コアを使用してコードの生成と最適化を行います。Binaryen の IR WebAssembly のサブセットであるため、WebAssembly にコンパイルされます。

Binaryen のオプティマイザーには、コードのサイズと速度の改善に役立つ多くのパスがあります。これらの 最適化の目的は、Binaryen をコンパイラとして十分パワフルに 内部 IP アドレスを使用しますまた、WebAssembly 固有の最適化( 汎用コンパイラでは実行されない可能性があります)。これは Wasm と考えることができます。 圧縮します。

Binaryen のサンプル ユーザーとしての AssemblyScript

Binaryen は多くのプロジェクトで使用されています。たとえば、 AssemblyScript。Binaryen を使用して以下を行います。 TypeScript に似た言語から直接 WebAssembly にコンパイルできます。 例を見る AssemblyScript プレイグラウンドにあります。

AssemblyScript 入力:

export function add(a: i32, b: i32): i32 {
  return a + b;
}

Binaryen によって生成されたテキスト形式の対応する WebAssembly コード:

(module
 (type $0 (func (param i32 i32) (result i32)))
 (memory $0 0)
 (export "add" (func $module/add))
 (export "memory" (memory $0))
 (func $module/add (param $0 i32) (param $1 i32) (result i32)
  local.get $0
  local.get $1
  i32.add
 )
)

前の例に基づいて生成された WebAssembly コードが表示されている AssemblyScript プレイグラウンド。

Binaryen ツールチェーン

Binaryen ツールチェーンには、JavaScript と JavaScript の両方に対応する便利なツールが数多く用意されています。 コマンドラインユーザーを指定できますこれらのツールの一部は、 説明します。 含まれているツールの一覧 プロジェクトの README ファイルで参照できます。

  • binaryen.js: Binaryen メソッドを公開するスタンドアロン JavaScript ライブラリ Wasm モジュールの作成と最適化。 ビルドについては、npm の binaryen.js をご覧ください。 (または、直接ダウンロードすること) GitHub または unpkg)。
  • wasm-opt: WebAssembly を読み込んで Binaryen IR を実行するコマンドライン ツール 渡します。
  • wasm-aswasm-dis: アセンブルと逆アセンブルを行うコマンドライン ツール WebAssembly
  • wasm-ctor-eval: 関数(または関数の一部)を実行できるコマンドライン ツール 関数など)をコンパイル時に使用します。
  • wasm-metadce: フレキシブル クラスタで Wasm ファイルの一部を削除するコマンドライン ツール モジュールの使用方法によります。
  • wasm-merge: 複数の Wasm ファイルを 1 つに統合するコマンドライン ツール 同様に、対応するインポートをエクスポートに接続します。たとえば、 Bundler ですが、Wasm 用です。

WebAssembly にコンパイルする

ある言語から別の言語にコンパイルするには、一般的に複数のステップが必要です。 重要なものを次のリストに示します。

  • 語彙分析: ソースコードをトークンに分割します。
  • 構文分析: 抽象構文ツリーを作成します。
  • セマンティック分析: エラーをチェックし、言語ルールを適用します。
  • 中間のコード生成: より抽象的な表現を作成します。
  • コード生成: ターゲット言語に翻訳します。
  • ターゲット固有のコード最適化: ターゲットに合わせて最適化します。

Unix の世界でよく使用されるコンパイル ツールは、 lexyacc:

  • lex(Lexical Analyzer Generator): lex は語彙を生成するツールです。 アナライザ(レクサーまたはスキャナとも呼ばれます)を使用します。一定の基準を定め 対応するアクションを入力として受け取り、特定のアクションを 入力ソースコード内のパターンを認識する語彙アナライザ。
  • yacc(Yet Another Compiler Compiler): yacc は、 パーサーを組み込みました。標準的な文法の記述に従って、 入力として使用し、パーサーのコードを生成します。パーサー 一般的に生成 抽象構文ツリー (AST)です。ソースコードの階層構造を表します。
で確認できます。

実例

この投稿の範囲では、プログラム全体を網羅することは不可能です。 わかりやすいように、ごく限定的で役に立たない ExampleScript と呼ばれる合成プログラミング言語です。 具体的な例で説明します。

  • add() 関数を記述するには、任意の加算の例をコーディングします。次に例を示します。 2 + 3
  • multiply() 関数を記述するには、たとえば 6 * 12 と記述します。

事前警告によると、まったく役に立たないものの、語彙や アナライザを単一の正規表現 /\d+\s*[\+\-\*\/]\s*\d+\s*/ に変更します。

次に、パーサーが必要です。これは、 正規表現を使用して抽象構文ツリーを作成し、 名前付きキャプチャ グループ: /(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/

ExampleScript コマンドは 1 行に 1 つずつなので、パーサーはコードを処理できます。 改行文字で区切って行ごとに分割します。これで 1 つ目の 上記の箇条書きリストの 3 つのステップ、つまり字句分析構文 分析セマンティック分析の 3 つがあります。これらの手順のコードは、 次の掲載情報です。

export default class Parser {
  parse(input) {
    input = input.split(/\n/);
    if (!input.every((line) => /\d+\s*[\+\-\*\/]\s*\d+\s*/gm.test(line))) {
      throw new Error('Parse error');
    }

    return input.map((line) => {
      const { groups } =
        /(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/gm.exec(
          line,
        );
      return {
        firstOperand: Number(groups.first_operand),
        operator: groups.operator,
        secondOperand: Number(groups.second_operand),
      };
    });
  }
}

中級者向けのコード生成

ExampleScript プログラムを抽象構文ツリーとして表現できるようになったので、 (非常に簡素化されていますが)次のステップでは、 中間表現です。最初のステップは Binaryen で新しいモジュールを作成します

const module = new binaryen.Module();

抽象構文ツリーの各行には、3 つの要素で構成される firstOperandoperatorsecondOperand。4 つの選択肢のそれぞれに ExampleScript の演算子である +-*/ という新しい 関数をモジュールに追加する必要がある これを Binaryen の Module#addFunction() メソッドと併用します。各 Pod のパラメータ Module#addFunction() メソッドは次のとおりです。

  • name: string。関数の名前を表します。
  • functionType: Signature。関数のシグネチャを表します。
  • varTypes: Type[]。指定された順序で、追加のローカルを指定します。
  • body: Expression(関数の内容)。

もう少し詳しく説明します Binaryen のドキュメント スペースを移動する方法について説明しますが、最終的には ExampleScript の + が 複数の演算子の 1 つとして Module#i32.add() メソッドに 使用可能 整数演算。 加算には 2 つのオペランド、1 つ目と 2 つ目の合計が必要です。対象: 関数を呼び出せるようにするには、 エクスポート済み Module#addFunctionExport() と一緒に使用できます。

module.addFunction(
  'add', // name: string
  binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
  binaryen.i32, // results: Type
  [binaryen.i32], // vars: Type[]
  //  body: ExpressionRef
  module.block(null, [
    module.local.set(
      2,
      module.i32.add(
        module.local.get(0, binaryen.i32),
        module.local.get(1, binaryen.i32),
      ),
    ),
    module.return(module.local.get(2, binaryen.i32)),
  ]),
);
module.addFunctionExport('add', 'add');

抽象構文ツリーを処理した後、モジュールには 4 つのメソッドが含まれます。 整数を処理する 3 つ、つまり Module#i32.add() に基づく add() subtract()Module#i32.sub()に基づく)、multiply()(以下に基づく) Module#i32.mul()、および Module#f64.div() に基づく外れ値 divide() ExampleScript は浮動小数点数の結果でも動作します

for (const line of parsed) {
      const { firstOperand, operator, secondOperand } = line;

      if (operator === '+') {
        module.addFunction(
          'add', // name: string
          binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
          binaryen.i32, // results: Type
          [binaryen.i32], // vars: Type[]
          //  body: ExpressionRef
          module.block(null, [
            module.local.set(
              2,
              module.i32.add(
                module.local.get(0, binaryen.i32),
                module.local.get(1, binaryen.i32)
              )
            ),
            module.return(module.local.get(2, binaryen.i32)),
          ])
        );
        module.addFunctionExport('add', 'add');
      } else if (operator === '-') {
        module.subtractFunction(
          // Skipped for brevity.
        )
      } else if (operator === '*') {
          // Skipped for brevity.
      }
      // And so on for all other operators, namely `-`, `*`, and `/`.

実際のコードベースを扱う場合、まったく機能しないデッドコードが生じることがあります。 渡されます。デッドコードを人為的に導入すること (後のステップで除外)は、ExampleScript の実行例で Wasm にコンパイルすると、エクスポートされていない関数を追加することで機能します。

// This function is added, but not exported,
// so it's effectively dead code.
module.addFunction(
  'deadcode', // name: string
  binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
  binaryen.i32, // results: Type
  [binaryen.i32], // vars: Type[]
  //  body: ExpressionRef
  module.block(null, [
    module.local.set(
      2,
      module.i32.div_u(
        module.local.get(0, binaryen.i32),
        module.local.get(1, binaryen.i32),
      ),
    ),
    module.return(module.local.get(2, binaryen.i32)),
  ]),
);

コンパイラの準備はほぼ整いました。厳密に必要なわけではありませんが、 Google Cloud の モジュールを検証する Module#validate() メソッドに置き換えます。

if (!module.validate()) {
  throw new Error('Validation error');
}

結果の Wasm コードの取得

宛先 結果の Wasm コードを取得する Binaryen には、2 つのメソッドが テキスト表現 S-expression.wat ファイルとして扱う 読み取れる形式にし、 バイナリ表現 .wasm ファイルとしてダウンロードし、ブラウザで直接実行できます。バイナリコードは、 ブラウザで直接実行できます。正常に機能したことを確認するために、エクスポートを できます。

const textData = module.emitText();
console.log(textData);

const wasmData = module.emitBinary();
const compiled = new WebAssembly.Module(wasmData);
const instance = new WebAssembly.Instance(compiled, {});
console.log('Wasm exports:\n', instance.exports);

4 つすべての ExampleScript プログラムの完全なテキスト表現 以下にオペレーションを示します。デッドコードがまだ残っていることに注目してください。 スクリーンショットに示されているように、 WebAssembly.Module.exports()

(module
 (type $0 (func (param i32 i32) (result i32)))
 (type $1 (func (param f64 f64) (result f64)))
 (export "add" (func $add))
 (export "subtract" (func $subtract))
 (export "multiply" (func $multiply))
 (export "divide" (func $divide))
 (func $add (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.add
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $subtract (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.sub
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $multiply (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.mul
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $divide (param $0 f64) (param $1 f64) (result f64)
  (local $2 f64)
  (local.set $2
   (f64.div
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
 (func $deadcode (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (local.set $2
   (i32.div_u
    (local.get $0)
    (local.get $1)
   )
  )
  (return
   (local.get $2)
  )
 )
)

加算、除算、乗算、減算の 4 つの関数が表示されている WebAssembly モジュール エクスポートの DevTools コンソールのスクリーンショット(公開されていないデッドコードを除く)。

WebAssembly の最適化

Binaryen には、Wasm コードを最適化する 2 つの方法が用意されています。1 つは Binaryen.js 自体のもので、 1 つはコマンドライン用です前者は標準的な最適化セットを適用し、 デフォルトで有効になっており、最適化レベルと縮小レベルを設定できます。また、 後者の場合、デフォルトではルールは使用されませんが、完全なカスタマイズが可能です。 十分なテストを行えば 最適な結果が得られます

Binaryen.js を使用した最適化

Binaryen を使用して Wasm モジュールを最適化する最も簡単な方法は、 Binaryen.js の Module#optimize() メソッドを直接呼び出します。必要に応じて 「 最適化レベルと縮小レベルを使用します。

// Assume the `wast` variable contains a Wasm program.
const module = binaryen.parseText(wast);
binaryen.setOptimizeLevel(2);
binaryen.setShrinkLevel(1);
// This corresponds to the `-Os` setting.
module.optimize();

これにより、以前に人為的に混入した古いコードが削除され、 ExampleScript のサンプルの Wasm バージョンのテキスト表現 なくなっていきます。また、local.set/get ペアが 最適化の手順 SimplifyLocals (その他の地域関連の最適化)と、 掃除機 (明らかに不要なコードが削除され)、returnRemoveUnusedBrs (不要な位置から挿入点を削除します)。

 (module
 (type $0 (func (param i32 i32) (result i32)))
 (type $1 (func (param f64 f64) (result f64)))
 (export "add" (func $add))
 (export "subtract" (func $subtract))
 (export "multiply" (func $multiply))
 (export "divide" (func $divide))
 (func $add (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (local.get $0)
   (local.get $1)
  )
 )
 (func $subtract (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
  (i32.sub
   (local.get $0)
   (local.get $1)
  )
 )
 (func $multiply (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
  (i32.mul
   (local.get $0)
   (local.get $1)
  )
 )
 (func $divide (; has Stack IR ;) (param $0 f64) (param $1 f64) (result f64)
  (f64.div
   (local.get $0)
   (local.get $1)
  )
 )
)

Google の 最適化パス Module#optimize() は、特定の最適化レベルと縮小レベルを使用して、デフォルト 学習します完全なカスタマイズを行うには、コマンドライン ツール wasm-opt を使用する必要があります。

wasm-opt コマンドライン ツールを使用した最適化

使用するパスを完全にカスタマイズするために、Binaryen には wasm-opt コマンドライン ツール。特典を 利用可能な最適化オプションの一覧 ツールのヘルプメッセージを確認してくださいwasm-opt ツールはおそらく最も人気があるでしょう 複数のコンパイラ ツールチェーンで使用され、Wasm コードを最適化しています。 (EmscriptenJ2CLKotlin/Wasmdart2wasmwasm-pack などです。

wasm-opt --help

このパスについて、おわかりになるよう、 専門的な知識がなくても理解できる:

  • CodeFolding: コードをマージして重複を回避します(たとえば、2 つの if 共通の指示がテスト群の端で示されています)。
  • DeadArgumentElimination: リンク時間最適化パスで引数を削除します。 常に同じ定数で呼び出される場合は、関数に代入する必要があります。
  • MinifyImportsAndExports: "a""b" に圧縮します。
  • DeadCodeElimination: デッドコードを削除します。

また、 最適化クックブック 適切なフラグを特定するためのヒントをいくつか紹介します。 重要であり、最初に試す価値があります。たとえば、wasm-opt を実行する場合があります。 入力がさらに縮小されます。このような場合は、 新しい --converge フラグ それ以上の最適化が行われなくなって固定小数点になるまで、反復処理が繰り返されます。 できます。

デモ

この投稿で紹介したコンセプトを実際に確認するには、 ExampleScript に自由に入力できます。また デモのソースコードをご覧ください

まとめ

Binaryen は、WebAssembly や WebAssembly などの言語をコンパイルするための 結果のコードを最適化しますJavaScript ライブラリとコマンドライン ツール 柔軟性と使いやすさを提供しますこの投稿では Google Cloud における Wasm コンパイル。Binaryen の有効性と 最大限に最適化できますBinaryen のシステムをカスタマイズするオプションの多くは 最適化を行うには Wasm の内部構造、 すでに問題なく動作しています。これでコンパイルと最適化が Binaryen とのコラボレーション

謝辞

この投稿は Alon Zakai がレビューし、 Thomas LivelyRachel Andrew