JavaScript 工作原理 —— 什么是 WebAssembly + WebAssembly 的使用场景
潘忠显 / 2021-04-06
“JavaScript 工作原理”系列文章是翻译和整理自 SessionStack 网站的 How JavaScript works。因为博文发表于2017年,部分技术或信息可能已经过时。本文英文原文链接,作者 Alexander Zlatkov,翻译 潘忠显。
这是致力于探索 JavaScript 及其构建组件的系列文章的第6部分,本文将分析 WebAssembly 工作原理,还将重点从性能方面分析 WebAssembly 与 JavaScript 之间的差异:加载时间、执行速度、垃圾回收、内存使用、平台API访问、调试、多线程以及可移植性。
我们构建 Web 应用程序的方式正处于革命的边缘:虽然这仍处于起步阶段,但我们对 Web 应用程序的思考方式正发生变化。
WebAssembly 是什么
WebAssembly 或称 wasm 是一个实验性的低级编程语言,应用于浏览器内的客户端。WebAssembly是便携式的抽象语法树,被设计来提供比JavaScript更快速的编译及运行。WebAssembly 将让开发者能运用自己熟悉的编程语言(最初以C/C++作为实现目标)编译,再藉虚拟机引擎在浏览器内运行。
First, let’s see what WebAssembly does
WebAssembly 或称 wasm 是一种实验性的、高效的 Web 底层字节码。WebAssembly 将让开发者能运用自己熟悉的编程语言(最初以 C/C++ 作为实现目标)编译,再借由虚拟机引擎在浏览器内运行,这将产生一个加载和执行速度非常快的网络应用。
(译注:补充资料,来自维基百科)
WebAssembly 的开发团队分别来自 Mozilla、Google、Microsoft、Apple,代表着四大网络浏览器 Firefox、Chrome、Microsoft Edge、Safari。2017年11月,以上四个浏览器都开始实验性的支持 WebAssembly。WebAssembly 于 2019 年 12 月 5 日成为万维网联盟(W3C)的推荐,与 HTML,CSS 和 JavaScript 一起,成为 Web 的第四种语言。
加载时间
为了加载 JavaScript,浏览器必须加载所有文本的 .js
文件。
因为通过网络传输过来的 WebAssembly 是已经编译完成的,因此它可以被更快的加载浏览器当中。wasm 是一种类似于汇编的低级语言,具有非常紧凑的二进制格式。
执行
目前,wasm 运行速度仅比 native code (译注:已被编译为特定于处理器的机器码的代码)执行慢 20%。从各个角度来看,这都是一个惊人的结果。wasm 格式被编译到沙盒环境中,并且在很多约束条件下运行,以确保它没有安全漏洞或针对这些漏洞进行了强化。wasm 是相比真正的 native code 速度减慢是最小的,而且将来会更快。
更重要的是 wasm 与浏览器无关,所有主流引擎都增加了对 WebAssembly 的支持,并且执行时间相当。
为了能理解 WebAssembly 与 JavaScript 相比执行得更快,您应该首先阅读之前的文章《V8引擎透视》。
让我们快速了解一下 V8 中的情况:
上图左侧是包含了一些函数的 JavaScript 源代码。首先,需要对其进行解析,以便将所有字符串转换为标记,并生成一个抽象语法树 (AST)。 AST 是 JavaScript 程序逻辑的内存表示形式。生成此表示后,V8 会直接使用机器代码。遍历树并生成机器代码,然后就有了已编译的函数。这里并没有真正的尝试来加快它。
现在,让我们看一下 V8 管道在下一阶段的功能:
上图右侧中的 TurboFan,是 V8 的优化编译器之一。当 JavaScript 应用程序运行时,很多代码都是在 V8 引擎中运行。TurboFan 监视代码是否运行缓慢、是否存在瓶颈和热点,以便对其进行优化。它将它们推向后端 (backend),这是一种经过优化的即时编译(just-in-time compilation, JIT),可为那些大量消耗 CPU 的函数创建更快的代码。
这在一定程度上能够解决问题,但分析代码并确定哪些需要优化的过程也消耗了CPU。反过来,这意味着更高的电量消耗,尤其在移动设备上更为明显。
wasm 则不需要这一切:它被插入工作流中,如下所示:
wasm 不仅在编译阶段已经进行了优化,也不需要解析。一个优化过的二进制文件,可以直接挂载到可以生成机器代码的后端。所有优化都由前端的编译器完成。
因为跳过了该过程中的许多步骤,使得 wasm 的执行效率更高。
内存模型
以编译成 WebAssembly 的 C++ 程序为例,其内存是一个连续的内存块,中间没有“空洞”。wasm 的特性之一是能够通过执行堆栈与线性内存分开的概念,来提高安全性。在 C++ 程序中,有一个从内存底部进行分配“堆”,还有一个从往顶部生长的“栈”。通过一个指针,可以处理您不应该接触的、内存中的变量。这是许多恶意软件利用这种陷阱。
WebAssembly 采用了完全不同的模型。执行堆栈与 WebAssembly 程序本身是分开的,因此,开发者无法在其中进行修改或更改变量等内容。另外,这些函数使用整数偏移量,而不是指针。函数指向间接函数表,然后,这些直接计算出的数字就会跳入模块内部的函数中。通过这种方式构建的,可以并排加载多个 wasm 模块,偏移所有索引,让每个模块都正常工作。
有关 JavaScript 中的内存模型和管理的更多信息,您可以查看《【JS 工作原理】03. 内存管理 + 4类常见内存泄漏的处理》。
垃圾收集
您已经知道 JavaScript 的内存管理是由垃圾收集器处理的,WebAssembly 略有不同。WebAssembly 支持手动管理内存的语言,开发者可以将自己的 GC 与wasm 模块一起传送,不过这是一项复杂的任务。
目前,WebAssembly 是围绕 C++ 和 RUST 用例设计的。由于 wasm 是低层次语言,它是汇编语言上边的一层,因此很容易变异成汇编语言,这一点很关键。 C 可以使用普通的 malloc
,C++ 可以使用智能指针,Rust使用完全不同的范例(一个完全不同的主题)。这些语言都不使用 GC,因此它们不需要复杂的运行时组件来跟踪内存,WebAssembly非常这些语言。
此外,这些语言并非 100% 为调用复杂的 JavaScript 之类功能(如对DOM进行更改)而设计的。用 C++ 编写整个 HTML 应用程序是没有意义的,因为 C++ 不是为此设计的。在大多数情况下,工程师编写 C++ 或 Rust 的目的是 WebGL 或高度优化的库(重度计算)。
未来 WebAssembly 将支持不附带的 GC 语言(??)。
平台接口访问
根据执行,将公开访问于的,这些访问可以通过JavaScript应用程序直接访问。
有赖于 JavaScript 的运行时,JavaScript 应用可以访问平台特定 API。比如在浏览器中运行 JavaScript,Web 应用程序可以调用一组 Web APIs,来控制网络浏览器/设备功能,并访问 DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等。
WebAssembly 模块无法访问任何平台API。一切都由需要 JavaScript 的传递。如果在 WebAssembly 模块中访问某些平台特定的 API,则必须通过 JavaScript 进行调用。
例如,如果要使用 console.log
,则必须通过 JavaScript 而不是 C++ 代码来调用它,这些 JavaScript 调用要付出一定的开销代价。但不会一直这样下去,未来该规范会为 wasm 提供平台 API,开发者能够在不使用 JavaScript 的情况下发布应用。
Source Maps
压缩 JavaScript 代码时,您需要一种适当地调试它的方法,Source Maps 就是来帮助做这件事的。
基本上,Source Map 是一种将组合/缩小的文件映射回未构建状态的方法。当构建生产环境所用的文件时,会产生最小化的、合并的 JavaScript 文件,同时还会生成一个 source map,其中包含有关原始文件的信息。可以通过 source map 在生成的 JavaScript 文件中,查询特定的行号和列号时,得到原始位置。
由于没有规范,WebAssembly 当前不支持 source map,但最终会(可能很快)支持。在 C++ 代码中设置断点时,您会看到 C++ 代码而不是 WebAssembly,至少目标是这样的。
多线程
JavaScript 在单个线程上运行。《【JS 工作原理】04. 事件循环和异步编程》中非常详细地介绍了使用事件循环和利用异步编程的方法。
JavaScript 在一些特例情况下也使用 Web Worker:基本上任何阻塞主 UI 线程的密集 CPU 计算,都可以通过转移到 Web Worker 来提高效率。但是,Web Workers 是无法访问 DOM 的。
WebAssembly 当前不支持多线程,以后可能会支持更接近原生的线程(如 C++ 风格的线程)。拥有“真实”线程,将在浏览器中创造许多新机会,这也可能导致更多的滥用。
可移植性
如今,JavaScript 几乎可以在任何地方运行,从浏览器到服务器端,甚至可以在嵌入式系统中运行。
WebAssembly 被设计为安全且可移植的,就像 JavaScript 一样,它将在支持 wasm 的各种环境中运行(例如,每个浏览器)。
WebAssembly 的可移植性目标与 Java 在 Applets 早期尝试实现的可移植性目标相同。
使用WebAssembly替代JavaScript的场景
使用 wasm 的场景主要包括:
- Web 游戏。在 WebAssembly 的第一个版本中,主要关注繁重的 CPU 绑定计算(例如处理数学运算)。游戏需要操纵大量像素,开发者可以使用捆绑有 OpenGL 的 C++ 或 Rust 来编写应用程序,然后将其编译为 wasm,最终在浏览器中运行。
- 计算密集型的库,如图像处理库,这会在性能方面带来有意义的提升
- 降低电池消耗的应用(取决于引擎),之前有介绍大多数处理步骤都是在编译过程中提前完成的,wasm 可以大大减少移动设备上的电池消耗
- 包管理,如 NPM (开发者也可以在实际上并未编写可编译代码的情况下,使用 wasm 二进制文件???)
(译注:关于 wasm 的游戏,可以看看 AngryBots,使用的是 Unity 引擎。之前的示例游戏链接失效了,这里随便找了个替换,可以点击查看更多的 demo)
对于频繁进行 DOM 操作和平台 API 的调用的应用,使用 JavaScript 绝对是合理的,因为它不会增加额外的开销,并且可以直接访问原生 API。