自从 Web 成为不仅用于文档,也用于应用的平台以来,一些最先进的应用已经将 Web 浏览器推向了极限。为了提高性能,通过与较低级别的语言接口“更接近底层”的方法在许多更高级别的语言中都会遇到。例如,Java 具有 Java Native Interface。对于 JavaScript,这种较低级别的语言是 WebAssembly。在本文中,您将了解什么是汇编语言,以及为什么它在 Web 上可能有用,然后了解 WebAssembly 是如何通过 asm.js 的临时解决方案创建的。
汇编语言
您是否曾经用汇编语言编程?在计算机编程中,汇编语言通常简称为汇编,通常缩写为 ASM 或 asm,是任何低级编程语言,该语言中的指令与体系结构的机器代码指令之间具有非常强的对应关系。
例如,查看 Intel® 64 和 IA-32 架构 (PDF),MUL
指令(用于multiplication,即乘法)执行第一个操作数(目标操作数)和第二个操作数(源操作数)的无符号乘法,并将结果存储在目标操作数中。非常简化地说,目标操作数是位于寄存器 AX
中的隐式操作数,而源操作数位于通用寄存器(如 CX
)中。结果再次存储在寄存器 AX
中。请考虑以下 x86 代码示例
mov ax, 5 ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx ; Multiply the value of register AX (5)
; and the value of register CX (10), and
; store the result in register AX.
为了进行比较,如果任务是使 5 和 10 相乘,您可能会在 JavaScript 中编写类似于以下内容的代码
const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;
采用汇编方式的优势在于,这种低级且机器优化的代码比高级且人为优化的代码效率更高。在前面的示例中,这无关紧要,但您可以想象,对于更复杂的操作,差异可能会很大。
顾名思义,x86 代码依赖于 x86 架构。如果有一种编写汇编代码的方法不依赖于特定架构,但会继承汇编的性能优势,那会怎么样呢?
asm.js
编写不具有架构依赖性的汇编代码的第一步是 asm.js,它是 JavaScript 的严格子集,可以用作编译器的低级、高效目标语言。这种子语言有效地描述了用于 C 或 C++ 等内存不安全语言的沙盒虚拟机。静态和动态验证的组合使 JavaScript 引擎能够为有效的 asm.js 代码采用预先(AOT)优化编译策略。用静态类型语言(具有手动内存管理,例如 C)编写的代码由源到源编译器(例如 早期的 Emscripten(基于 LLVM))进行翻译。
通过将语言功能限制为适合 AOT 的功能,提高了性能。Firefox 22 是第一个 支持 asm.js 的浏览器,以 OdinMonkey 的名称发布。Chrome 在版本 61 中添加了 asm.js 支持。虽然 asm.js 仍然可以在浏览器中使用,但它已被 WebAssembly 取代。在这一点上使用 asm.js 的原因将是作为不支持 WebAssembly 的浏览器的替代方案。
WebAssembly
WebAssembly 是一种类似于汇编的低级语言,具有紧凑的二进制格式,可以接近原生性能运行,并为 C/C++ 和 Rust 等语言以及更多语言提供编译目标,以便它们可以在 Web 上运行。对 Java 和 Dart 等内存管理语言的支持正在开发中,应该很快就会可用,或者已经像 Kotlin/Wasm 的情况一样已经实现。WebAssembly 旨在与 JavaScript 并行运行,从而允许两者协同工作。
除了浏览器之外,由于 WASI(WebAssembly 系统接口,WebAssembly 的模块化系统接口),WebAssembly 程序也可以在其他运行时中运行。创建 WASI 的目的是使其可在操作系统之间移植,目标是安全且能够在沙盒环境中运行。
WebAssembly 代码(二进制代码,即字节码)旨在在可移植的虚拟堆栈机 (VM) 上运行。字节码旨在比 JavaScript 更快地解析和执行,并具有紧凑的代码表示形式。
指令的概念执行通过传统的程序计数器进行,该计数器在指令中前进。实际上,大多数 Wasm 引擎将 Wasm 字节码编译为机器代码,然后执行该代码。指令分为两类
- 控制指令,用于形成控制结构,并从堆栈中弹出其参数值,可能会更改程序计数器,并将结果值推入堆栈。
- 简单指令,用于从堆栈中弹出其参数值,将运算符应用于这些值,然后将结果值推入堆栈,然后隐式地推进程序计数器。
回到之前的示例,以下 WebAssembly 代码将等效于本文开头的 x86 代码
i32.const 5 ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul ; Pop the two most recent items on the stack,
; multiply them, and push the result onto the stack.
虽然 asm.js 完全在软件中实现,也就是说,它的代码可以在任何 JavaScript 引擎中运行(即使未优化),但 WebAssembly 需要所有浏览器供应商都同意的新功能。于 2015 年宣布并于 2017 年 3 月首次发布,WebAssembly 于 2019 年 12 月 5 日成为 W3C 推荐标准。W3C 在所有主要浏览器供应商和其他相关方的贡献下维护该标准。自 2017 年以来,浏览器支持已普及。
WebAssembly 有两种表示形式:文本和二进制。您在上面看到的是文本表示形式。
文本表示形式
文本表示形式基于 S-表达式,通常使用文件扩展名 .wat
(代表 WebAssembly text format,即 WebAssembly 文本格式)。如果您真的想这样做,您可以手动编写它。从上面的乘法示例入手,并通过不再硬编码因子使其更有用,您可能会理解以下代码
(module
(func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
local.get $factor1
local.get $factor2
i32.mul)
(export "mul" (func $mul))
)
二进制表示形式
使用文件扩展名 .wasm
的二进制格式并非供人阅读,更不用说人工创建了。使用诸如 wat2wasm 之类的工具,您可以将上面的代码转换为以下二进制表示形式。(注释通常不是二进制表示形式的一部分,而是由 wat2wasm 工具添加的,以便更好地理解。)
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01 ; section code
0000009: 00 ; section size (guess)
000000a: 01 ; num types
; func type 0
000000b: 60 ; func
000000c: 02 ; num params
000000d: 7f ; i32
000000e: 7f ; i32
000000f: 01 ; num results
0000010: 7f ; i32
0000009: 07 ; FIXUP section size
; section "Function" (3)
0000011: 03 ; section code
0000012: 00 ; section size (guess)
0000013: 01 ; num functions
0000014: 00 ; function 0 signature index
0000012: 02 ; FIXUP section size
; section "Export" (7)
0000015: 07 ; section code
0000016: 00 ; section size (guess)
0000017: 01 ; num exports
0000018: 03 ; string length
0000019: 6d75 6c mul ; export name
000001c: 00 ; export kind
000001d: 00 ; export func index
0000016: 07 ; FIXUP section size
; section "Code" (10)
000001e: 0a ; section code
000001f: 00 ; section size (guess)
0000020: 01 ; num functions
; function body 0
0000021: 00 ; func body size (guess)
0000022: 00 ; local decl count
0000023: 20 ; local.get
0000024: 00 ; local index
0000025: 20 ; local.get
0000026: 01 ; local index
0000027: 6c ; i32.mul
0000028: 0b ; end
0000021: 07 ; FIXUP func body size
000001f: 09 ; FIXUP section size
; section "name"
0000029: 00 ; section code
000002a: 00 ; section size (guess)
000002b: 04 ; string length
000002c: 6e61 6d65 name ; custom section name
0000030: 01 ; name subsection type
0000031: 00 ; subsection size (guess)
0000032: 01 ; num names
0000033: 00 ; elem index
0000034: 03 ; string length
0000035: 6d75 6c mul ; elem name 0
0000031: 06 ; FIXUP subsection size
0000038: 02 ; local name type
0000039: 00 ; subsection size (guess)
000003a: 01 ; num functions
000003b: 00 ; function index
000003c: 02 ; num locals
000003d: 00 ; local index
000003e: 07 ; string length
000003f: 6661 6374 6f72 31 factor1 ; local name 0
0000046: 01 ; local index
0000047: 07 ; string length
0000048: 6661 6374 6f72 32 factor2 ; local name 1
0000039: 15 ; FIXUP subsection size
000002a: 24 ; FIXUP section size
编译为 WebAssembly
如您所见,.wat
和 .wasm
都不是特别人性化。这就是像 Emscripten 这样的编译器发挥作用的地方。它使您可以从更高级别的语言(如 C 和 C++)进行编译。还有用于其他语言(如 Rust 和更多语言)的其他编译器。考虑以下 C 代码
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
通常,您将使用编译器 gcc
编译此 C 程序。
$ gcc hello.c -o hello
在 安装 Emscripten 后,您可以使用 emcc
命令和几乎相同的参数将其编译为 WebAssembly
$ emcc hello.c -o hello.html
这将创建一个 hello.wasm
文件和 HTML 包装器文件 hello.html
。当您从 Web 服务器提供 hello.html
文件时,您将在 DevTools 控制台中看到打印的 "Hello World"
。
还有一种无需 HTML 包装器即可编译为 WebAssembly 的方法
$ emcc hello.c -o hello.js
与之前一样,这将创建一个 hello.wasm
文件,但这次是一个 hello.js
文件,而不是 HTML 包装器。要进行测试,请例如使用 Node.js 运行生成的 JavaScript 文件 hello.js
$ node hello.js
Hello World
了解更多
这篇对 WebAssembly 的简短介绍只是冰山一角。在 MDN 上的 WebAssembly 文档中了解有关 WebAssembly 的更多信息,并查阅 Emscripten 文档。说实话,使用 WebAssembly 可能感觉有点像 如何画猫头鹰的梗,特别是考虑到熟悉 HTML、CSS 和 JavaScript 的 Web 开发者不一定精通 C 等要编译的语言。幸运的是,有一些渠道,例如 StackOverflow 的 webassembly
标签,如果您礼貌地提问,专家通常很乐意提供帮助。
鸣谢
本文由 Jakob Kummerow、Derek Schuff 和 Rachel Andrew 审阅。