自从网络成为文档平台和应用程序平台以来,一些最高级的应用程序已将网络浏览器推向极限。许多高级别的语言都遇到通过与较低级别的语言进行交互以“更接近金属”来提高性能的方法。例如,Java 具有 Java 原生接口。对于 JavaScript,这种较低级别的语言是 WebAssembly。在本文中,您将了解什么是汇编语言,以及它为什么能在 Web 上发挥作用,然后学习如何通过 asm.js 的临时解决方案创建 WebAssembly。
汇编语言
您是否曾用汇编语言进行编程?在计算机编程中,汇编语言通常简称为汇编语言,通常缩写为 ASM 或 asm,是任何低阶编程语言,语言指令与架构的机器代码指令之间存在极强的对应关系。
例如,以 Intel® 64 和 IA-32 架构 (PDF) 为例,MUL
指令(用于运算)对第一个运算数(目标运算数)和第二个运算数(源运算数)执行无符号乘法,并将结果存储在目标运算数中。非常简单:目标运算数是位于寄存器 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)编写的代码由源到源编译器进行翻译,例如 early 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 System Interface,即 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 需要所有浏览器供应商都同意的新功能。WebAssembly 于 2015 年发布,于 2017 年 3 月首次发布,并于 2019 年 12 月 5 日成为 W3C 建议。W3C 通过所有主要浏览器供应商和其他相关方做出的贡献来维护该标准。从 2017 年开始,Google 已普遍支持浏览器。
WebAssembly 有两种表示法:文本和二进制。您在上面看到的是文本表示。
文本表示
文本表示法基于 S 表达式,通常采用文件扩展名 .wat
(适用于网络组合文字格式)。如果确实需要,您可以手写输入。以上面的乘法示例为例,通过不再对因子进行硬编码以使其更加有用,您或许能够理解以下代码:
(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
。从网络服务器提供文件 hello.html
时,您会看到开发者工具控制台中输出了 "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 的简要介绍只是冰山一角。如需详细了解 WebAssembly,请参阅 MDN 的 WebAssembly 文档,并参阅 Emscripten 文档。老实说,使用 WebAssembly 可能会有点像如何绘制猫头鹰表情包,尤其是熟悉 HTML、CSS 和 JavaScript 的网络开发者不一定精通 C 语言等要编译的语言。幸运的是,像 StackOverflow 的 webassembly
标签这样一些频道,只要您提出要求,专家通常很乐意为您提供帮助。
致谢
本文由 Jakob Kummerow、Derek Schuff 和 Rachel Andrew 审核。