使用 Binaryen 編譯 Wasm 並進行最佳化

Binaryen 是編譯器和工具鍊 WebAssembly 的基礎架構程式庫,以 C++ 編寫而成。目的在於讓模型 編譯為 WebAssembly 的直覺式、快速且有效。本文章中,使用 名為 ExampleScript 的合成玩具語言範例,瞭解如何寫出 在 JavaScript 中使用 Binaryen.js API 的 WebAssembly 模組。我們將會介紹 模組建立、函式補充和匯出的基本概念 函式。這樣您就能瞭解 將實際程式設計語言編譯到 WebAssembly 的機制另外 您會學到如何使用 Binaryen.js 和 指令列中加上 wasm-opt

二進位檔背景

Binaryen 提供 C API 也會傳回單一標頭中的 從 JavaScript 使用 這個語言接受輸入 WebAssembly 表單, 也接受 控制流程圖 比較適合採用此架構的編譯器

中繼表示法 (IR) 是指資料結構或程式碼 由編譯器或虛擬機器執行內部程式碼,藉此表示原始碼。二進位 內部 IR 採用精簡的資料結構,專為完全平行而設計 產生及最佳化程式碼,運用所有可用的 CPU 核心。二進位的 IR 由於屬於 WebAssembly 的一部分 因此編譯成 WebAssembly

二進位檔案最佳化器有許多票證,可以改善程式碼大小和速度。這些 最佳化功能的目標是讓 Binaryen 具備足夠的功能來做為編譯器使用 後端。包括專為 WebAssembly 設計的最佳化功能 一般用途編譯器可能不適用),您可以將其視為 Wasm 壓縮。

AssemblyScript 做為 Binaryen 的範例使用者

二進位檔案會由多項專案使用,例如 AssemblyScript: 從類似 TypeScript 的語言直接編譯至 WebAssembly。 試用範例 安裝在 AssemblyScript 遊樂場

AssemblyScript 輸入內容:

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

以二進位檔案產生的文字格式對應的 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
 )
)

AssemblyScript Playground 顯示根據上述範例產生的 WebAssembly 程式碼。

二進位工具鍊

二進位工具鍊提供許多適用於 JavaScript 的實用工具 開發人員和指令列使用者其中部分工具列於 下列;這個 完整的內建工具清單 適用於專案的 README 檔案。

  • binaryen.js:公開 Binaryen 方法的獨立 JavaScript 程式庫 的 建立及最佳化 Wasm 模組。 如需建構項目,請參閱 npm 上的 binaryen.js。 (或直接從以下網址下載 GitHubunpkg)。
  • wasm-opt:可載入 WebAssembly 並執行二進位 IR 的指令列工具 傳回
  • wasm-aswasm-dis:組合及拆解的指令列工具 WebAssembly。
  • wasm-ctor-eval:可以執行函式 (或 函式)。
  • wasm-metadce:指令列工具,可彈性調整 Wasm 檔案中的部分內容 視模組的使用方式而定。
  • wasm-merge:可將多個 Wasm 檔案合併為單一 Wasm 檔案的指令列工具 檔案,將對應的匯入檔案連結至匯出。如 Bundler 適用於 JavaScript,但適用於 Wasm

編譯至 WebAssembly

編譯另一種語言通常需要幾個步驟,且 下列清單已列出重要的變更:

  • Lexical 分析:將原始碼切割為符記。
  • 語法分析:建立抽象語法樹狀結構。
  • 語意分析:檢查錯誤並強制執行語言規則。
  • 產生中階程式碼:建立更抽象的表示法。
  • 程式碼產生:翻譯成譯文語言。
  • 特定目標的程式碼最佳化:針對目標進行最佳化。

在 Unix 環境中,常用的編譯工具是 lexyacc:

  • lex (Lexical Analyzer 產生器): lex 是能產生詞法分析的工具 分析工具,也稱為 lexer 或掃描器這個過程需要一組 和對應的動作做為輸入內容 能辨識輸入原始碼中模式的詞彙分析工具。
  • 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 指令每行一個,因此剖析器能夠處理程式碼 分行輸入換行字元。這足以檢查 項目符號清單的三個步驟,即詞法分析語法 分析語意分析。這些步驟的程式碼位於 下列商家資訊。

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();

抽象語法樹狀結構中的每一行都包含三個由 firstOperandoperatorsecondOperand。我們針對這四個可能性 範例:+-*/、新的 函式需新增至模組 搭配二進位的 Module#addFunction() 方法。Deployment 的參數 Module#addFunction() 方法如下:

  • namestring,代表函式名稱。
  • functionTypeSignature,代表函式的簽名。
  • varTypesType[],以特定順序表示其他當地語言。
  • bodyExpression,函式的內容。

想放鬆和休息一下 Binaryen 說明文件 可協助你瀏覽聊天室,但最終,對於 ExampleScript 的 + 運算子,最終會得到 Module#i32.add() 方法的數個 可供使用 整數運算。 加法需要兩個運算元,第一個和第二個加總。對於 函式才能確實呼叫 已匯出 搭配 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');

處理抽象語法樹狀結構後,模組會包含四個方法 三個處理整數,也就是以 Module#i32.add() 為基礎的 add()subtract()multiply()根據Module#i32.sub() Module#i32.mul(),以及離群值 divide() (根據 Module#f64.div()) 因為 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)),
  ]),
);

編譯器現在即將準備就緒。這不是強制要求,可是非常必要的 驗證模組 呼叫 Module#validate() 方法

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

取得產生的 Wasm 程式碼

目的地: 取得產生的 Wasm 程式碼 二進位檔案提供兩個方法 文字表示法 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);

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)
  )
 )
)

WebAssembly 模組匯出的開發人員工具螢幕截圖,顯示四個函式:加減法,但不含公開的無效程式碼。

最佳化 WebAssembly

Binaryen 提供兩種將 Wasm 程式碼最佳化的方法。一個位於 Binaryen.js 本身, 一個用於指令列前者會套用標準最佳化組合 可設定最佳化和縮減層級 後者預設不會使用任何規則,但允許完全自訂 也就是說,只要您有足夠的實驗結果 您就能針對不同的廣告客戶 根據您的程式碼傳回最佳結果

使用 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 會如何移除 local.set/get 組合 最佳化步驟 SimplifyLocals (其他當地相關最佳化) 和 吸塵器 (移除明顯不需要的程式碼),而 return 會由 RemoveUnusedBrs (移除不需要的位置)。

 (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)
  )
 )
)

有許多 最佳化程序 Module#optimize() 則採用特定最佳化和縮減程度預設 運用 AI 開發原則審查系統如要完整自訂,您必須使用指令列工具 wasm-opt

使用 wasm-opt 指令列工具進行最佳化

如要完整自訂要使用的票證,Binaryen 提供 wasm-opt 指令列工具。若要取得 可選的最佳化選項完整清單 查看該工具的說明訊息。wasm-opt 工具可能是 ,並由多個編譯器工具鍊使用,最佳化 Wasm 程式碼、 包括 EmscriptenJ2CLKotlin/Wasmdart2wasmwasm-pack 等。

wasm-opt --help

我們整理出幾項票證 使用者無需專業知識就能理解

  • CodeFolding:避免合併重複的程式碼 (例如有兩個 if 時), 每個選項的結尾處也會提供相關指示)。
  • DeadArgumentElimination:透過連結時間最佳化傳遞來移除引數 表示一律使用相同的常數呼叫函式。
  • MinifyImportsAndExports:將其壓縮為 "a""b"
  • DeadCodeElimination:移除無效程式碼。

還有 最佳化教戰手冊 我們就另外提供一些秘訣 協助您辨別 值得優先嘗試例如,有時執行 wasm-opt 一再重複地一再重複縮小輸入範圍,即可進一步縮小輸入範圍。在這種情況下,執行 使用 --converge 標記 會持續疊代,直到最佳化作業無法進一步最佳化 。

示範

如要查看本文介紹的概念,請使用嵌入式 示範任何能想到的 ExampleScript 輸入項目另外也請 查看示範的原始碼

結論

Binaryen 提供功能強大的工具包,可將語言編譯至 WebAssembly,並 最佳化產生的程式碼其 JavaScript 程式庫和指令列工具 來提供彈性與易用性這篇文章說明瞭 Wasm 編譯,凸顯貝倫對以下方面的成效與潛力: 盡量提高最佳化成效自訂二進位檔案的許多選項 如要進行最佳化,就必須深入瞭解 Wasm 的內部結構, 預設設定就能正常運作輕鬆編寫及最佳化 和 Binaryen 說道!

特別銘謝

這篇文章是由 Alon Zakai 審核, Thomas LivelyRachel Andrew