【译】是什么能让 WebAssembly 这么快? —— WebAssembly 系列(五)

译者:Mactavish(博主本人)

链接:http://www.zcfy.cc/article/4162

原文:https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast

这是 “WebAssembly 以及为什么它这么快” 这个系列的第五部分。如果你还没阅读其他的部分,我们建议你 从头开始阅读

上篇文章,里我讲述了利用 WebAssembly 或者 JavaScript 来编程并不是一个二选一的情况。我们并不期望有过多的开发者来编写全是 WebAssembly 代码的代码库。

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

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

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

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

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

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

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

Diagram showing 5 categories of work in current JS engines

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

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

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

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

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

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

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

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

Diagram showing 3 categories of work in past JS engines (parse, execute, and garbage collection) with times being much longer than previous diagram

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

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

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

WebAssembly 要如何比较?

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

Diagram showing 3 categories of work in WebAssembly (decode, compile + optimize, and execute) with times being much shorter than either of the previous diagrams

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

抓取

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

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

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

解析

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

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

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

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

Diagram comparing parsing in current JS engine with decoding in WebAssembly, which is shorter

编译 + 优化

正如我在关于JIT的文章里解释的那样,JavaScript 是在代码执行的时候编译的。取决于运行时所需要的类型,同样的代码的不同版本可能需要多次编译。

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

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

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

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

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

Diagram comparing compiling + optimizing, with WebAssembly being shorter

重优化

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

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

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

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

Diagram showing that reoptimization happens in JS, but is not required for WebAssembly

执行

书写高性能的 JavaScript 是可行的。为了达到这个目的,你需要了解 JIT 执行的优化。比如,你需要知道如何编写能够让编译器能轻易地类型特化的代码。正如我在这篇文章中谈论的一样。

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

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

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

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

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

Diagram comparing execution, with WebAssembly being shorter

垃圾回收

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

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

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

Diagram showing that garbage collection happens in JS, but is not required for WebAssembly

总结

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

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

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

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

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

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

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

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

在一些情况下,WebAssembly表现也不及预期,也有一些即将到来的变化能使它更加地快。我会在 下一篇文章中 涵盖那些内容。

关于

Lin Clark

Lin 是 Mozilla Developer Relations 团队的一名工程师。 She 专注于 JavaScript, WebAssembly, Rust, 以及 Servo,同时也绘制一些关于编码的漫画。

Lin Clark 的更多文章

转载于:https://my.oschina.net/JSBreaker/blog/1536929

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值