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
)
)
Binaryen 工具链
Binaryen 工具链提供了许多适用于 JavaScript 和
开发者和命令行用户这些工具中的一部分工具列在
关注;该
所含工具的完整列表
可在项目的 README
文件中找到。
binaryen.js
:一个独立的 JavaScript 库,用于公开 Binaryen 方法 用于 如何创建和优化 Wasm 模块。 如需了解构建,请参阅 npm 上的 binaryen.js (也可以直接从 GitHub 或 unpkg)。wasm-opt
:用于加载 WebAssembly 并运行 Binaryen IR 的命令行工具 传递。wasm-as
和wasm-dis
:用于组装和反汇编的命令行工具 WebAssembly。wasm-ctor-eval
:可用于执行函数(或 函数)。wasm-metadce
:用于在柔性环境中移除 Wasm 文件部分内容的命令行工具 具体取决于模块的使用方式。wasm-merge
:用于将多个 Wasm 文件合并为单个的命令行工具 文件,在此过程中将相应的导入连接到导出。类似于 适用于 JavaScript 但适用于 Wasm 的 bundler。
编译为 WebAssembly
将一种语言编译为另一种语言通常涉及几个步骤,其中最主要 下面列出了一些重要的工具:
- 词汇分析:将源代码拆分为词法单元。
- 语法分析:创建抽象语法树。
- 语义分析:检查错误并强制执行语言规则。
- 中间代码生成:创建更抽象的表示法。
- 代码生成:翻译为目标语言。
- 针对特定目标的代码优化:针对目标进行优化。
在 Unix 环境中,常用的编译工具是
lex
和
yacc
:
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();
抽象语法树的每一行都包含一个三元组,由
firstOperand
、operator
和secondOperand
。对于
运算符,即 +
、-
、*
、/
、新的
需要向该模块中添加函数
。Module#addFunction()
参数的
Module#addFunction()
方法如下所示:
name
:一个string
,表示函数的名称。functionType
:一个Signature
,表示函数的签名。varTypes
:Type[]
,表示按指定顺序的其他局部变量。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
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 代码,
包括 Emscripten、
J2CL、
Kotlin/Wasm、
dart2wasm、
wasm-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 Lively 和 Rachel Andrew。