本文译者360齐武剧团前端高级开发工程师原标题:是什么让WebAssembly速度快?原作者:LinClark原文地址:https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/这是WebAssembly系列文章的第五篇。如果你还没有读过其他的,我建议你从头开始。在之前的一篇文章中,我解释了WebAssembly和JavaScript这两种技术并非不兼容。我们不希望开发人员编写纯WebAssembly代码库。也就是说,开发者在开发应用时不需要在WebAssembly和JavaScript之间取舍。但我们希望开发者可以将项目中的部分JavaScript代码切换到WebAssembly版本。例如,React开发团队可以用WebAssembly版本替换他们的协调器代码(又名虚拟DOM)。使用React的人无需做任何事情……他们的应用程序将像以前一样工作,受益于WebAssembly。由于WebAssembly的执行速度更快,React团队被说服进行转换。但其执行速度更快的原因是什么?JavaScript目前的性能如何?在了解JavaScript和WebAssembly的性能差异之前,我们需要先了解一下JS引擎是干什么的。下面我通过图来简单描述下今天应用的启动。JS引擎在每个任务上花费的时间取决于页面上使用的JavaScript代码。插图不代表准确的性能数据。相反,该图提供了一个高级模型来比较JS和WebAssembly完成相同功能的不同性能。这些水平条代表完成特定任务所需的时间。解析——将源代码转换成解释器可以运行的代码所花费的时间。编译+优化——花在优化编译器和基线编译器上的时间。优化编译器的一些工作不是在主线程上完成的,所以这些任务不包括在这里。重新优化——如果JIT发现自己不符合假设,则需要重新调整。这里的调整包括重新优化和反优化(将优化后的代码还原为基线代码)。执行——运行代码所花费的时间。垃圾回收——清理内存所花费的时间。这里很重要的一点是,这些任务执行并不是完全离散的,也不遵循特定的顺序。相反,它们是交织在一起的。做一些解析,然后编译,然后再解析,再执行等等。这样的工作分割给JavaScript带来了巨大的性能提升,早期的JavaScript可能类似于这样:早期的JavaScript只依赖于解释器,而执行速度很慢。引入JIT后,执行速度有了很大的提升。权衡之后,虽然需要付出监控和编译的开销,但开发者可以在不改动代码的情况下缩短解析和编译的时间。然而,性能的提高促使开发人员创建更大的JavaScript应用程序。这意味着仍有改进的余地。WebAssembly相比如何?我估计了与使用WebAssembly相比,传统Web应用程序的外观。不同浏览器处理这些阶段的方式可能略有不同。我将在这里使用SpiderMonkey作为示例。拉取的阶段图中没有画出来,但是实际上从服务器拉取文件是一件非常耗时的事情。由于WebAssembly的压缩率高于JavaScript,因此拉取速度会更快。即使通过压缩算法显着减小了JavaScript的包大小,它也不会小于WebAssembly的压缩二进制表示形式。这意味着服务器和客户端之间的传输速度更快。这在慢速网络上尤为明显。JavaScript源码解析后拉到浏览器,会转换成AST(抽象语法树)。浏览器将始终延迟执行,只解析第一件事,并为未调用的函数创建“存根”。之后,将AST转化为JS引擎专用的IR(称为字节码)。相比之下,WebAssembly不需要这种转换,因为它已经是IR。只需解码它并检查它是否有错误。编译+优化正如我在JIT文章中解释的那样,JavaScript在代码执行时对其进行编译。根据运行时使用的类型,可能需要编译相同代码的不同版本。不同的浏览器编译WebAssembly的方式不同。一些浏览器在开始执行之前对WebAssembly进行基线编译,而其他浏览器则使用JIT。不管怎样,WebAssembly从一开始就更接近于机器代码。以程序中包含的类型为例。它更快的原因是因为编译器在开始编译优化代码之前不必花时间执行代码来观察正在使用的类型。编译器不需要根据观察到的不同类型来编译相同代码的不同版本。LLVM中还进行了其他一些优化。这样编译和优化代码的工作就轻松了。重新优化有时JIT必须丢弃代码的优化版本并重新优化。当JIT根据正在执行的代码发现它的假设是错误的时,就会发生这种情况。例如,当进入循环的变量在某个迭代期间发生变化时,或者当函数被插入原型链时,就会发生去优化。去优化有两个代价。首先,放弃优化代码并回退到基线版本需要时间。其次,如果函数仍然被多次调用,JIT可能会选择再次将函数代码发送给优化编译器重新编译,而这样的二次编译又是一个成本。在WebAssembly中,类型之类的东西是显式的,因此JIT不需要根据运行时数据做出假设。这意味着WebAssembly不需要经历重新优化周期。可以编写高性能的JavaScript。为此,您需要了解JIT所做的优化。例如,您需要知道如何编写代码,以便编译器可以指定类型,正如我在JIT文章中所说的那样。然而,大多数开发人员并不了解JIT的内部细节。即使对于那些了解JIT内部细节的开发人员来说,也很难将程序调整到最佳状态。许多有助于提高代码可读性的编程范例(例如,将通用业务抽象为可以跨类型的函数)会阻止编译器优化代码。此外,不同的浏览器使用JIT进行不同的优化,因此在一个浏览器中针对内部细节进行优化可能会使您的代码在另一个浏览器中表现不佳。因此,执行WebAssembly代码通常更快。WebAssembly不需要许多针对JavaScript的JIT优化(如类型特异性)。此外,WebAssembly被设计为编译器目标。换句话说,WebAssembly被设计成一个编译器生成的产品,而不是由人类开发人员来编写它。由于人类程序员不需要直接编写WebAssembly,WebAssembly提供了对机器更友好的指令集。根据执行的代码类型,这些指令可以帮助将速度提高10%到800%。垃圾收集在JavaScript中,开发人员不必担心从内存中清除不需要的旧变量。JS引擎会自动使用垃圾收集器来处理这个。如果您想获得可预测的性能,这可能会很困难。您无法控制垃圾收集器的工作,因此它可能会在不适当的时间启动。大多数浏览器在安排清理方面做得很好,但垃圾收集仍然是代码执行的开销。至少目前,WebAssembly根本不支持垃圾回收。内存是手动管理的(如C和C++)。虽然这会增加开发者的开发难度,但也让程序的性能更加稳定。总结由于以下因素,WebAssembly执行得更快:拉取WebAssembly可以节省时间。因为WebAssembly的压缩率比JavaScript高。解码WebAssembly比解析JavaScript花费的时间更少。编译和优化时间更短。因为WebAssembly比JavaScript更接近机器码,而且WebAssembly在服务端做了优化。将不再发生重新优化。由于WebAssembly在编译时内置了类型和其他信息,因此JS引擎不需要像JavaScript那样在优化期间推测类型。更短的执行时间。因为开发者不需要为了更稳定的性能学习编译器技巧,而且WebAssembly的指令集对机器更友好。不依赖于垃圾回收。因为内存是手动管理的。这就是我认为WebAssembly在相同任务上优于JavaScript的地方。在某些情况下,WebAssembly不会按预期执行。此外,WebAssembly有一些即将发生的变化,这些变化将使其更快。我将在下一篇文章中谈谈这些要点。
