(转)WASM(WebAssember)快速了解第五篇——是什么使WebAssembly很快
这是有关WebAssembly的系列文章的第五部分,如果您还没有阅读其他文章,我们建议从头开始。
在上一篇文章中,我解释了使用WebAssembly或JavaScript进行编程不是一种选择。我们不希望有太多的开发人员编写完整的WebAssembly代码库。
因此,开发人员无需为其应用程序在WebAssembly和JavaScript之间进行选择。但是,我们确实希望开发人员可以将部分JavaScript代码换成WebAssembly。
例如,React团队可以用WebAssembly版本替换其协调器代码(又名虚拟DOM)。使用React的人不需要做任何事情……他们的应用程序可以像以前一样正常工作,除了可以从WebAssembly中受益。
像React团队一样的开发人员之所以会进行这种交换,是因为WebAssembly更快。但是什么使它更快?
今天的JavaScript性能如何?
在我们了解JavaScript和WebAssembly之间的性能差异之前,我们需要了解JS引擎所做的工作。
该图粗略地描述了应用程序的启动性能现在看起来会是什么样。
JS引擎花费在执行这些任务中的任何一项上的时间取决于页面使用的JavaScript。该图并不表示精确的性能数字。相反,它旨在提供一个高级模型,说明JS与WebAssembly中相同功能的性能如何不同。
每个条形图显示了完成特定任务所花费的时间。
- 解析(parse)——将源代码处理为解释程序可以运行的时间。
- 编译(compile)+优化(optimize)——在基准编译器和优化编译器上花费的时间。优化编译器的某些工作不在主线程上,因此此处未包括在内。
- 重新优化(re-optimize)——假设失败时,JIT进行重新调整花费的时间,既重新优化代码又将优化后的代码返回到基线代码。
- 执行(execute)——运行代码所需的时间。
- 垃圾回收(garbage collection)——清理内存所花费的时间。
重点关注的一点:这些任务不会以离散的块或特定的顺序发生。相反,它们将会交错的处理。整个过程会先进行一些解析,然后执行一些,然后进行编译,然后进行更多的解析,然后进行更多的执行,依此类推。
与JavaScript早期相比,这种故障带来的性能有了很大的改进,而JavaScript看起来更像是这样:
最初,当它只是运行JavaScript的解释器时,执行速度非常慢。引入JIT时,它大大缩短了执行时间。
权衡是监视和编译代码的开销。如果JavaScript开发人员继续以与他们相同的方式编写JavaScript,则解析和编译时间将很小。但是性能的提高导致开发人员创建了更大的JavaScript应用程序。
这意味着仍有改进的空间。
WebAssembly如何比较?
这是WebAssembly如何与典型Web应用程序进行比较的近似值。
浏览器在处理所有这些阶段的方式上略有不同。我在这里使用SpiderMonkey作为模型。
获取
该图未显示,但是需要花费时间的一件事就是从服务器获取文件。
因为WebAssembly比JavaScript更紧凑,所以获取它更快。即使压缩算法可以显著减小JavaScript包的大小,WebAssembly的压缩二进制表示形式仍然较小。
这意味着花费更少的时间在服务器和客户端之间进行传输。在慢速网络上尤其如此。
解析中
一旦到达浏览器,JavaScript源就会被解析为抽象语法树。
浏览器通常懒惰地执行此操作,首先只是解析他们真正需要的内容,而只是为尚未被调用的功能创建存根。
从那里,AST被转换为特定于该JS引擎的中间表示(称为字节码)。
相反,WebAssembly不需要进行此转换,因为它已经是中间表示。只需对其进行解码和验证,以确保其中没有任何错误。
编译+优化
正如我在有关JIT的文章中所解释的那样,JavaScript是在代码执行期间编译的。根据运行时使用的类型,可能需要编译同一代码的多个版本。
不同的浏览器对WebAssembly的编译方式不同。一些浏览器在开始执行WebAssembly之前会对其进行基线编译,而其他浏览器则使用JIT。
无论哪种方式,WebAssembly的起点都更接近于机器代码。例如,类型是程序的一部分。由于以下几个原因,这样做速度更快:
- 编译器在开始编译优化的代码之前不必花时间运行代码来观察正在使用的类型。
- 编译器不必根据观察到的不同类型来编译同一代码的不同版本。
- LLVM中已经提前完成了更多优化。因此,只需很少的工作即可进行编译和优化。
重新优化
有时,JIT必须抛出优化版本的代码,然后重试。
当JIT基于运行代码做出的假设被证明是不正确的时,就会发生这种情况。例如,当进入循环的变量与先前迭代中的变量不同时,或者在原型链中插入新函数时,就会发生去优化。
取消优化有两个成本。首先,要花一些时间才能退出优化的代码并返回到基准版本。其次,如果仍然经常调用该函数,则JIT可能决定再次通过优化编译器发送该函数,因此存在第二次编译该函数的成本。
在WebAssembly中,诸如类型之类的东西是显式的,因此JIT不需要根据其在运行时收集的数据对类型进行假设。这意味着它不必经历重新优化周期。
执行中
可以编写高效执行的JavaScript。为此,您需要了解JIT所做的优化。例如,您需要知道如何编写代码,以便编译器可以对其进行类型化处理,如有关JIT的文章中所述。
但是,大多数开发人员都不了解JIT内部。即使对于确实了解JIT内部知识的开发人员,也很难达到最佳效果。人们尝试使代码更具可读性的许多编码模式(例如,将常见任务抽象为可跨类型工作的函数)会在尝试优化代码时妨碍编译器的工作。
另外,JIT使用的优化在浏览器之间是不同的,因此对一种浏览器的内部进行编码可能会使您的代码在另一种浏览器中的性能降低。
因此,通常在WebAssembly中执行代码更快。WebAssembly不需要JIT对JavaScript进行的许多优化(例如类型特殊化)。
另外,WebAssembly被设计为编译器目标。这意味着它是为编译器生成的,而不是供人类程序员编写的。
由于人类程序员不需要直接对其进行编程,因此WebAssembly可以提供一组更适合机器的指令。根据您代码的工作类型,这些指令的运行速度可提高10%至800%。
垃圾收集
在JavaScript中,开发人员不必担心在不再需要旧变量时从内存中清除它们。相反,JS引擎会使用称为垃圾收集器的方法自动执行此操作。
但是,如果您想要可预测的性能,则可能会遇到问题。您无法控制垃圾收集器何时进行工作,因此它可能会在不方便的时候出现。大多数浏览器在调度它方面都非常擅长,但是这仍然会影响代码的执行。
至少到目前为止,WebAssembly根本不支持垃圾回收。内存是手动管理的(就像使用C和C ++这样的语言)。尽管这可能会使开发人员的编程变得更加困难,但也确实使性能更加一致。
结论
在许多情况下,WebAssembly比JavaScript更快,因为:
- 提取WebAssembly所需的时间更少,因为即使压缩后,它也比JavaScript更紧凑。
- 解码WebAssembly所需的时间少于解析JavaScript所需的时间。
- 编译和优化所需的时间更少,因为WebAssembly比JavaScript更接近机器代码,并且已经在服务器端进行了优化。
- 不需要进行重新优化,因为WebAssembly内置了类型和其他信息,因此JS引擎在优化JavaScript方式时无需推测。
- 执行通常会花费较少的时间,因为开发人员在编写一致的高性能代码时需要知道的编译器技巧和陷阱就更少了,另外WebAssembly的指令集更适合机器。
- 由于内存是手动管理的,因此不需要垃圾回收。
这就是为什么在很多情况下,WebAssembly在执行相同任务时会胜过JavaScript。
在某些情况下,WebAssembly的性能不能达到预期的效果,并且即将出现的一些变化将使其变得更快。我将在下一篇文章中介绍。
转自:https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/