最近,WebAssembly
在 JavaScript
圈非常的火!搬砖小弟关注的各大前端公众号都在疯狂的推送这类文章,不得不让人深思,人们都在谈论它多么多么快,怎样怎样改变 Web
开发领域。但是没有人讲他到底为什么那么快。在这篇文章里,我将会帮你了解 WebAssembly
到底是什么。
当我们了解事务时,都会抛出三个问题:
WebAssembly
是什么?WebAssembly
怎么样工作的?WebAssembly
做了这件事有什么影响?
那么下面就开始从这个方面开始展开对WebAssembly
的探讨吧,希望能让你对WebAssembly的认识更进一步。
WebAssembly是什么?
WebAssembly
是一种可以使用非 JavaScript
编程语言编写代码并且能在浏览器上运行的技术方案。
当大家谈论起 WebAssembly
时,首先想到的就是 JavaScript
。现在,我们没有必须在 WebAssembly 和 JavaScript 中选一个的意思。实际上,我们期待开发者在一个项目中把 WebAssembly 和 JavaScript 结合使用。但是,比较这两者是有用的,这对你了解 WebAssembly 有一定帮助。
亿点点性能历史
1995 年 JavaScript
诞生。它的设计时间非常短,前十年发展迅速。
紧接着浏览器厂商们就开始了更多的竞争。
2008年,人们称之为浏览器性能大战的时期开始了。很多浏览器加入了即时编译器,又称之为JITs
。在这种模式下,JavaScript在运行的时候,JIT
选择模式然后基于这些模式使代码运行更快。
这些 JITs
的引入是浏览器运行代码机制的一个转折点。所有的突然之间,JavaScript 的运行速度快了10倍。
随着这种改进的性能,JavaScript 开始被用于意想不到的事情,比如使用Node.js
和Electron
构建应用程序。
现在 WebAssembly
可能是的另一个转折点。
在我们没有搞清楚 JavaScript 和 WebAssembly 之间的性能差前,我们需要理解 JS 引擎所做的工作。
JavaScript 是如何在浏览器中运行的呢?
作为一个开发人员,您将JavaScript添加到页面时,您有一个目标并遇到一个问题。
目标:你想要告诉计算机做什么
问题:你和计算机使用的语言不同
您说的是人类的语言,计算机说的是机器语言。尽管你不认为 JavaScript 或者其他高级语言是人类语言,但事实就是这样的。它们的设计是为了让人们认知,不是为机器设计的。
所以JavaScript引擎的工作就是把你的人类语言转化成机器所理解的语言。
所以,怎么进行翻译呢?
在编程中,通常有两种翻译方法将代码翻译成机器语言。你可以使用解释器或者编译器。
使用解释器,翻译的过程基本上是一行一行及时生效的。
编译器是另外一种工作方式,它在执行前翻译。
每种翻译方法都有利弊。
解释器的利弊
- 解释器很快的获取代码并且执行。您不需要在您可以执行代码的时候知道全部的编译步骤。因此,解释器感觉与 JavaScript有着自然的契合。web 开发者能够立即得到反馈很重要。这也是浏览器最开始使用 JavaScript 解释器的原因之一。
- 但是实用解释器的弊端是当你运行相同的代码的时候。比如,你执行了一个循环。然后你就会一遍又一遍的做同样的事情。
编译器的利弊
- 编译器则有相反的效果。在程序开始的时候,它可能需要稍微多一点的时间来了解整个编译的步骤。但是当运行一个循环的时候他会更快,因为他不需要重复的去翻译每一次循环里的代码。
- 因为解释器必须在每次循环访问时不断重新转换代码,作为一个可以摆脱解释器低效率的方法,浏览器开始将编译器引入。
- 不同的浏览器实现起来稍有不同,但是基本目的是相同的。他们给 JavaScript 引擎添加了一个新的部分,称为监视器(也称为分析器)。该监视器在 JavaScript 运行时监控代码,并记录代码片段运行的次数以及使用了那些数据类型。如果相同的代码行运行了几次,这段代码被标记为 “warm”。如果运行次数比较多,就被标记为 “hot”。
耗时比较:JavaScript Vs. WebAssembly
这张图大致给出了现在一个程序的启动性能,目前 JIT 编译器在浏览器中很常见。
该图显示了 JS 引擎运行程序花费的时间。显示的时间并不是平均的。这个图片表明,JS 引擎做的这些任务花费的时间取决于页面中 JavaScript 做了什么事情。但是我们可以用这个图来构建一个心理模型。每栏显示花费在特定任务上的时间。
- Parsing - 讲源码转换成解释器可以运行的东西所用的事情。
- Compiling + optimizing - 花费在基础编译和优化编译上的时间。有一些优化编译的工作不在主线程,所以这里并不包括这些时间。
- Re-optimizing - 当预先编译优化的代码不能被优化的情况下,JIT 将这些代码重新优化,如果不能重新优化那么久丢给基础编译去做。这个过程叫做重新优化。
- Execution - 执行代码的过程
- Garbage collection - 清理内存的时间
一个重要的事情要注意:这些任务不会发生在离散块或特定的序列中。相反,它们将被交叉执行。比如正在做一些代码解析时,还执行者一些其他的逻辑,有些代码编译完成后,引擎又做了一些解析,然后又执行了一些逻辑,等等。
这种交叉执行对早期 JavaScript 的性能有很大的帮助,早期的 JavaScript 的执行就像下图一样:
一开始,当只有一个解释器运行 JavaScript 时,执行速度相当缓慢。JITs 的引入,大大提升了执行效率。
下面是 WebAssembly 如何比较典型 web 应用:
请求
- 下载执行与 JavaScript 等效的 WebAssembly 文件需要更少的时间,因为它的体积更小。WebAssembly 设计的体积更小,可以以二进制形式表示。
- 即使使用 gzip 压缩的 JavaScript文件很小,但 WebAssembly 中的等效代码可能更小。
解析
- JavaScript 源码一旦被下载到浏览器,源将被解析为抽象语法树(AST)。通常浏览器解析源码是懒惰的,浏览器首先会解析他们真正需要的东西,没有及时被调用的函数只会被创建成存根。在这个过程中,AST被转换为该 JS 引擎的中间表示(称为字节码)。
- 相反,WebAssembly 不需要被转换,因为它已经是字节码了。它仅仅需要被解码并确定没有任何错误。
编译 + 优化
- 如前所述,JavaScript 是在执行代码期间编译的。因为 JavaScript 是动态类型语言,相同的代码在多次执行中都有可能都因为代码里含有不同的类型数据被重新编译。这样会消耗时间。
- 相反,WebAssembly 与机器代码更接近。例如,类型是程序的一部分。
这是由于:
- 编译器不需要在运行代码时花费时间去观察代码中的数据类型,在开始编译时做优化。
- 编译器不需要去每次执行相同代码中数据类型是否一样。
- 更多的优化在 LLVM 最前面就已经完成了。所以编译和优化的工作很少。
重新优化
有时 JIT 抛出一个优化版本的代码,然后重新优化。
- JIT 基于运行代码的假设不正确时,会发生这种情况。例如,当进入循环的变量与先前的迭代不同时,或者在原型链中插入新函数时,会发生重新优化。
- 在 WebAssembly 中,类型是明确的,因此 JIT 不需要根据运行时收集的数据对类型进行假设。这意味着它不必经过重新优化的周期。
执行
尽可能编写执行性能好的 JavaScript。所以,你可能需要知道 JIT 是如何做优化的。
由于程序员不需要直接编程,WebAssembly 提供了一组更适合机器的指令。根据您的代码所做的工作,这些指令的运行速度可以在10%到800%之间。
垃圾回收
- 在 JavaScript 中,开发者不需要担心内存中无用变量的回收。JS 引擎使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。
- 这对于控制性能可能并不是一件好事。你并不能控制垃圾回收时机,所以它可能在非常重要的时间去工作,从而影响性能。
- 现在,WebAssembly 根本不支持垃圾回收。内存是手动管理的(就像 C/C++)。虽然这些可能让开发者编程更困难,但它的确提升了性能。
WebAssembly 是如何工作的?
现在,您了解开发人员为什么对 WebAssembly 感到兴奋,让我们来看看它是如何工作的。
当我谈到上面的 JIT 时,我谈到了与机器的沟通像与外星人沟通。
我现在想看看这个外星人的大脑如何工作 - 机器的大脑如何解析和理解交流内容。
负责思考的部分是算术逻辑单元(ALU)。
短期储存由寄存器(Registers)提供。
随机存储器(或RAM)来提供长期储存能力。
机器码中的语句被称为指令。
当一条指令进入大脑时会发生什么?它被拆分成了多个的部分并有特殊的含义。
例如,这个大脑从机器码中取出4-10位,并将它们发送到 ALU。ALU进行计算,它根据 0 和 1 的位置来确定是否需要将两个数相加。
这个块被称为“操作码”,因为它告诉 ALU 执行什么操作。
那么这个大脑会拿后面的两个块来确定他们所要操作的数。这两个块对应的是寄存器的地址。
请注意添加在机器码上面的标注(ADD R1 R2),这使我们更容易了解发生了什么。这就是汇编。它被称为符号机器码。这样人类也能看懂机器码的含义。
您可以看到,这个机器的汇编和机器码之间有非常直接的关系。每种机器内部有不同的结构,所以每种机器都有自己独有的汇编语言。
所以我们并不只有一个翻译的目标。
相反,我们的目标是不同类型的机器码。就像人类说不同的语言一样,机器也有不同的语言。
这样做的效率非常低。为了解决这个问题,大多数编译器会在高级语言和汇编语言之间多加一层。编译器将把高级语言翻译成一种更低级的语言,但比机器码的等级高。这就是中间代码(IR)。
意思就是编译器可以将任何一种高级语言转换成一种中间语言。然后,编译器的另外的部分将中间语言编译成目标机器的汇编代码。
编译器的“前端”将高级编程语言转换为IR。编译器的“后端”将 IR 转换成目标机器的汇编代码。
总结
使用WebAssembly,可以更快地在 web 应用上运行代码。这里有 几个 WebAssembly 代码运行速度比 JavaScript 高效的原因。
- 文件加载 - WebAssembly 文件体积更小,所以下载速度更快。
- 解析 - 解码 WebAssembly 比解析 JavaScript 要快
- 编译和优化 - 编译和优化所需的时间较少,因为在将文件推送到服务器之前已经进行了更多优化,JavaScript 需要为动态类型多次编译代码
- 重新优化 - WebAssembly 代码不需要重新优化,因为编译器有足够的信息可以在第一次运行时获得正确的代码
- 执行 - 执行可以更快,WebAssembly 指令更接近机器码
- 垃圾回收 - 目前 WebAssembly 不直接支持垃圾回收,垃圾回收都是手动控制的,所以比自动垃圾回收效率更高。
目前浏览器中的 MVP(最小化可行产品) 已经很快了。在接下来的几年里,随着浏览器的发展和新功能的增加,它将在未来几年内变得更快。没有人可以肯定地说,这些性能改进可以实现什么样的应用。但是,如果过去有任何迹象,我们可以期待惊奇。