使用 Binaryen 编译和优化 Wasm

Binaryen 是一种编译器和工具链 使用 C++ 编写的 WebAssembly 基础架构库。它旨在 直观、快速且高效。在本文中,使用 合成玩具语言示例,名为 ExampleScript,了解如何编写 使用 Binaryen.js API 以 JavaScript 编写的 WebAssembly 模块。您将介绍 关于模块创建、模块添加函数以及导出的基础知识 函数。这有助于您了解 将实际编程语言编译为 WebAssembly 的机制。此外, 您将学习如何使用 Binaryen.js 优化 Wasm 模块,以及如何使用 和 wasm-opt 搭配使用。

Binaryen 背景

Binaryen 具有直观的 C API 放在单个标题中,也可以 通过 JavaScript 使用。 它接受输入 WebAssembly 表单、 但它也接受 控制流图

中间表示法 (IR) 是指所使用的数据结构或代码 由编译器或虚拟机在内部表示源代码。Binaryen 内部 IR 使用紧凑的数据结构,旨在实现完全并行 使用所有可用的 CPU 核心生成和优化代码。Binaryen 的 IR 因为是 WebAssembly 的子集,所以会编译为 WebAssembly。

Binaryen 的优化器具有多次遍历,可以改进代码大小和速度。这些 优化旨在使 Binaryen 足够强大,可以用作编译器 一个后端。它包含特定于 WebAssembly 的优化( 而通用编译器则不然)。 缩减大小。

使用 AssemblyScript 作为 Binaryen 的示例用户

Binaryen 被许多项目使用,例如 AssemblyScript,该脚本使用 Binaryen 来 从类似 TypeScript 的语言直接编译为 WebAssembly。 试用示例 找到这个示例

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

AssemblyScript Playground 会显示基于上一个示例生成的 WebAssembly 代码。

Binaryen 工具链

Binaryen 工具链提供了许多适用于 JavaScript 和 开发者和命令行用户这些工具中的一部分工具列在 关注;该 所含工具的完整列表 可在项目的 README 文件中找到。

  • binaryen.js:一个独立的 JavaScript 库,用于公开 Binaryen 方法 用于 如何创建和优化 Wasm 模块。 如需了解构建,请参阅 npm 上的 binaryen.js (也可以直接从 GitHubunpkg)。
  • wasm-opt:用于加载 WebAssembly 并运行 Binaryen IR 的命令行工具 传递。
  • wasm-aswasm-dis:用于组装和反汇编的命令行工具 WebAssembly。
  • wasm-ctor-eval:可用于执行函数(或 函数)。
  • wasm-metadce:用于在柔性环境中移除 Wasm 文件部分内容的命令行工具 具体取决于模块的使用方式。
  • wasm-merge:用于将多个 Wasm 文件合并为单个的命令行工具 文件,在此过程中将相应的导入连接到导出。类似于 适用于 JavaScript 但适用于 Wasm 的 bundler。

编译为 WebAssembly

将一种语言编译为另一种语言通常涉及几个步骤,其中最主要 下面列出了一些重要的工具:

  • 词汇分析:将源代码拆分为词法单元。
  • 语法分析:创建抽象语法树。
  • 语义分析:检查错误并强制执行语言规则。
  • 中间代码生成:创建更抽象的表示法。
  • 代码生成:翻译为目标语言。
  • 针对特定目标的代码优化:针对目标进行优化。

在 Unix 环境中,常用的编译工具是 lexyacc:

  • lex(词汇分析器生成器)lex 是一种生成词法分析器的工具, 也称作词法分析器或扫描器它需要 表达式和相应的操作作为输入,并生成 用于识别输入源代码中模式的词法分析器。
  • yacc(另一种编译器编译器)yacc 是一种生成 用于语法分析的解析器它需要一个正式的语法描述, 作为输入,并为解析器生成代码。解析器 通常会生成 抽象语法树 (AST)。

有效的示例

鉴于这篇博文的范围,我们无法涵盖完整的编程内容 因此为简单起见,我们假设采用非常有限且无用的 一种叫做 ExampleScript 的合成编程语言,其工作原理是通过 通过具体示例进行泛化操作。

  • 如需编写 add() 函数,您需要编写一个任何加法示例的代码,例如: 2 + 3
  • 例如,要编写 multiply() 函数,您需要编写 6 * 12

正如之前所预告的那样,这完全没用,但就其词汇词汇而言就足够了 analyticsr 转换为单个正则表达式:/\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()参数的 Module#addFunction() 方法如下所示:

  • name:一个 string,表示函数的名称。
  • functionType:一个 Signature,表示函数的签名。
  • varTypesType[],表示按指定顺序的其他局部变量。
  • body:一个 Expression,即函数的内容。

这里有一些放松和分解的细节 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()(基于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 `/`.

在处理实际代码库时,有时候会遇到从未有过的死代码, 调用。人为引入死代码(会被优化并 (在后续步骤中已消除)中。 将编译到 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 代码, Binaryen 中提供了两种方法 文本表示S 表达式中的 .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 对通过 优化步骤 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() 会使用特定的优化和收缩级别默认 集。如需进行全面自定义,您需要使用命令行工具 wasm-opt

使用 wasm-opt 命令行工具进行优化

为了完全自定义要使用的卡券,Binaryen 包含 wasm-opt 命令行工具。要获得 可能的优化选项的完整列表, 请查看该工具的帮助消息wasm-opt 工具可能是最受欢迎的 并由多个编译器工具链用来优化 Wasm 代码, 包括 EmscriptenJ2CLKotlin/Wasmdart2wasmwasm-pack 等。

wasm-opt --help

为了让您感受一下通行证,下面摘录了 简单易懂:

  • CodeFolding:通过合并代码来避免重复代码(例如,如果两个 if 实验组会有一些共同的说明)。
  • DeadArgumentEliiation:用于移除参数的链接时间优化传递 如果始终使用相同的常量来调用函数,则会发生此错误。
  • MinifyImportsAndExports:将其缩减为 "a""b"
  • DeadCodeEliiation:移除死代码。

这里有 优化实战宝典 还提供了几条提示,帮助您识别 很重要,值得先尝试。例如,有时运行 wasm-opt 会进一步缩小输入。在这种情况下,运行 替换为 --converge 标志 不断迭代,直到没有进一步的优化,并且确定一个固定点 。

演示

如需了解这篇博文中介绍的概念的实际运用,不妨试用嵌入式 演示,在其中提供您能想到的任何 ExampleScript 输入。此外,请务必 查看演示的源代码

总结

Binaryen 提供了一个强大的工具包,用于将语言编译为 WebAssembly 和 优化所生成的代码。它的 JavaScript 库和命令行工具 灵活性和易用性此博文介绍了 Wasm 编译,凸显了 Binaryen 的有效性和潜力 最大优化。虽然用于自定义 Binaryen 文件的许多选项 要进行优化,需要对 Wasm 的内部构件有深入了解, 默认设置已经非常实用这样,您就能够顺利地编译和优化 使用 Binaryen!

致谢

此帖子由 Alon Zakai 审核, Thomas LivelyRachel Andrew