WebAssembly 为什么这么快?

379 阅读10分钟
原文链接: mp.weixin.qq.com

什么是 WebAssembly

WebAssembly 是一种使 JavaScript 以外的编程语言编写的代码能够在浏览器中运行的技术。所以当人们在讨论 WebAssembly 运行之快的时候, 实际上是在和 JavaScript 进行对比。

现在,我不会暗示说这是一个非此即彼的情况——你要么使用 WebAssembly 或者是 JavaScript。事实上,我们期望开发者能够在一个应用中同时使用 WebAssembly 与 JavaScript。

但是比较这两者还是很有用处的,它能够让我们了解 WebAssembly 带来的潜在影响。

关于性能的一点历史

JavaScript 创造于 1995。它并非被设计成一门以快为目的的语言,并且在头十年,它的确不快。

然后浏览器开始变得更有竞争力了。

在2008 年,被人们称作性能大战的时期开始了。各种浏览器加入了实时编译器,也被称为 JIT。在 JavaScript 运行的时候,JIT 能够分析出其中的模式并基于这些模式让代码更快地运行。

这些 JIT 的引入使得 JavaScript 的性能迎来了一个拐点。JS 的执行快了十倍甚至更多。

随着性能的提示,JavaScript 开始被用于原本谁都预想不到的地方,比如用 Node.js 实现服务端的编程。性能的提升使得 JavaScript 可以被用于解决新的种类的问题。

有了 WebAssembly,或许我们正处于另外一个拐点

那么,让我们深入细节,理解 WebAssembly 为什么这么快的原因。

WebAssembly 或者 JavaScript 来编程并不是一个二选一的情况。我们并不期望有过多的开发者来编写全是 WebAssembly 代码的代码库。

所以开发者们并不需要在开发应用时对 WebAssembly 与 JavaScript 之间做出选择。然而,我们期望看到开发者们能够将他们的一部分 JavaScript 代码切换成 WebAssembly

比如,React 团队可以将它们的协调器代码(也就是虚拟 DOM)替换成 WebAssembly 版本。而 React 的使用者并不受影响,他们的 app 依旧能够像往常一样正常运行,不过他们也能从 WebAssembly 获益。

像 React 团队的开发者们切换代码的原因是因为 WebAssembly 更快。那么它为何能这么快呢?

如今 JavaScript 的性能是什么状况?

在我们充分理解 JavaScript和 WebAssembly 之间的性能差异之前,我们需要理解 JS 引擎所做的工作。

这张图片粗略地展示了当今的应用程序的启动性能是什么样。

JS 引擎花在任何这些任务的时间取决于页面使用的 JavaScript。这张图并不代表精确的性能参数。它的意义在于提供了一个高级模型来阐述对于同样的功能,JS 对比 WebAssembly 之间的性能差异。

每一条都显示了特定任务所花费的时间。

  • 解析 — 将源码处理成解释器可以运行的东西所花费的时间。

  • 编译 + 优化 — 在基线编译器和优化编译器中所花费的时间。有一些优化编译器不再主线程运行,所以没有包括在这里。

  • 重优化 — 当 JIT 假定(编译器对代码结构的假设,以减少重复编译)失败的时候重新调整所花费的时间。包含重新优化和将之前优化过的代码跳回原来基本代码。

  • 执行 — 运行代码所花费的时间。

  • 垃圾回收 — 清理内存所花费的时间。

值得强调的是:这些任务并非在离散的块或者特定的序列里发生。 相反,它们是交错的。解析一小段,然后执行一小段,然后编译,然后又解析更多的代码,然后再执行更多的代码,诸如此类…

这种分离与早期的 JavaScript 的性能相比带来了很大的改进,早期的看起来像是这样:

最开始,只有一个解释器来运行 JavaScript,它的执行速度是非常慢的。当 JIT 被引入之后,它彻底地提升了执行的时间。

监测和编译代码的开销是折中的。如果 JavaScript 开发者一直用同样的方式编写 JavaScript,那么,解析和编译的时间就很短。但是性能的提升导致开发者们构建大型的 JavaScript 应用。

这意味着依然还有提升的空间。

WebAssembly 要如何比较?

这里有一个 WebAssembly 对一个典型的 web 应用的对比的估测。

不同的浏览器之间处理这些解析有着轻微的不同,在这里我以 SpiderMonkey 作为模型。

1. 抓取

这个过程并没有显示在图中,不过从服务器中抓取文件本来就是需要占用一些时间的一件事。

因为 WebAssembly 比 JavaScript 更为压缩,因此抓取速度也更快。虽然压缩算法可以显著地减少 JavaScript 打包文件的体积,使用压缩的二进制表示的 WebAssembly 仍然更胜一筹。

这意味着在客户端和服务器之间传输所花费的时间更少,特别是在缓慢的网络连接的情况下。

2. 解析一旦数据到达了浏览器,JavaScript 源码开始解析成一个抽象语法树(AST)。

浏览器经常惰性地做这件事,因为它值解析一开始它需要的东西,并且为没有被调用的函数只创建存根。

然后 AST 会被转化成特定 JS 引擎的一个中间表示(叫做字节码)。

相反,In contrast, WebAssembly 不需要经历着个转换过程,因为它本身就已经是中间表示了。它只需要被解码然后验证以保证没有任何错误在里面。

3. 编译 + 优化

JavaScript 是在代码执行的时候编译的。取决于运行时所需要的类型,同样的代码的不同版本可能需要多次编译。

不同浏览器处理Different browsers handle compiling WebAssembly 的编译也不同。一些浏览器在开始执行 WebAssembly 之前对它做一个基线编译,其他的则使用 JIT 。

无论哪种方式,WebAssembly 起始更接近机器码。打个比方,类型是程序的一部分。它更快的原因有:

  1. 编译器在开始编译优化的代码之前并不需要花时间去观察当前正在使用的是什么类型。

  2. 编译器不需要根据观察到的不同类型来对同样的代码编译不同的版本。

  3. 在 LLVM 时已经提前做了许多的优化。所以编译和优化的工作就相对更少了。

4. 重优化

有时候,JIT 会扔出一些优化过的代码然后尝试重新优化。

这个过程发生在当 JIT 依据正在运行的代码做出的假定是正确的时候。比如,去优化发生在循环里的变量和它先前迭代的时候不一样,或者是当一个新的函数被插入到原型链当中。

对去优化来说有两种成本。首先,它需要将优化过的代码退回基本版本。其次,如果某个函数仍然被多次调用,那么 JIT 可能会将它重新送至优化编译器,所以这就有了二次编译的成本。

在 WebAssembly 当中,类型是显式的,所以 JIT 不需要根据运行时收集的数据对类型做假设。 这意味着它不需要经历重优化的循环。

5. 执行书写高性能的 JavaScript 是可行的。为了达到这个目的,你需要了解 JIT 执行的优化。比如,你需要知道如何编写能够让编译器能轻易地类型特化的代码。

而,大多数开发者并不知道 JIT 的内部原理。就算那些开发者了解 JIT 的内部原理,仍然可能达不到目的。人们使用的一些使得代码可读性更强的编码模式(比如将通用任务抽象成为可以处理不同的数据类型的函数)反而在编译器优化代码的时候给编译器造成了麻烦。

此外,JIT 使用的优化手段在不同浏览器中是不同的,所以正对某个浏览器内部的原理的编码可能会造成在其他浏览器内的性能下降。

正因为如此,一般执行 WebAssembly 中的代码通常来说要更快。许多 JIT 针对 JavaScript 的优化(比如类型特化)对 WebAssembly 来说是完全没有必要的。

除此之外, WebAssembly 被设计成编译器的目标。这意味着它被设计成为编译器能够生成的,而不是人类程序员可以书写的。

由于人类程序员不需要直接对它编程,WebAssembly 可以提供一系列的对机器更加理想的指令。取决于你的代码的具体有着什么样的目的,这些指令的运行速度从 10% 到 800% 更快。

6. 垃圾回收在 JavaScript 当中,开发者不必担心变量再需要的时候去内存中清理它们。JS 引擎自动地使用了叫做垃圾回收器的东西来处理它们。

如果你需要可预测的性能,那么这样可能会出现一些问题。你无法控制什么时候垃圾回收器该工作,它可能会在一些不恰当的时机出现。大多数浏览器都很擅长调度它,但是它仍然有一些开销,它会阻碍代码的执行。

至少目前来说,WebAssembly 完全不支持垃圾回收。内存需要手动管理(就像 C 和 C++ 那样)。那么这样会使得编程对程序员来说更加困难,不过它确实能够使得性能更加一致。

总结

WebAssembly 在很多方面比 JavaScript 更快的原因是:

  • 抓取 WebAssembly 比 JavaScript 花费的时间更少,哪怕当它们都被压缩过。

  • 编码 WebAssembly 比解析 JavaScript 所花费的时间更少。

  • WebAssembly 比 JavaScript 更加接近机器码而且在服务端就已经经过了优化,所以它编译和优化需要的时间更少。

  • WebAssembly 不需要重优化,因为它有明确的类型以及内置的额外信息,所以 JS 引擎不需要像优化 JavaScript 那样对它进行推测。

  • 执行阶段花费的时间更少,开发者不必为了写出性能一致性更高的代码而去了解一些编译器的技巧和陷阱。而且 WebAssembly 的一系列的只能对机器来说更加理想。

  • 不需要垃圾回收机制,因为内存都是手动管理的。

这就是为什么在很多例子中,对于同样的任务,WebAssembly 的表现要比 JavaScript 更好。

本文是译文,原文链接是:https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/

「一个有温度的前端号」

长按识别二维码关注

点赞分享是对作者最大的支持!

推荐阅读

深入理解虚拟 DOM,它真的不快

尤大多伦多演讲:Vue 3.0 预览

说说前端未来几年的发展方向