初探WebAssembly

背景介绍

WebAssembly 在 JavaScript 圈非常的火!人们都在谈论它多么多么快,怎样怎样改变 Web 开发领域。但是没有人讲他到底为什么那么快。
WebAssembly 是一种可以使用非 JavaScript 编程语言编写代码并且能在浏览器上运行的技术方案。

当大家谈论起 WebAssembly 时,首先想到的就是 JavaScript。现在,我没有必须在 WebAssembly 和 JavaScript 中选一个的意思。实际上,我们期待开发者在一个项目中把 WebAssembly 和 JavaScript 结合使用。但是,比较这两者是有用的,这对你了解 WebAssembly 有一定帮助。

js性能历史

1995 年 JavaScript 诞生。它的设计时间非常短,前十年发展迅速。

紧接着浏览器厂商们就开始了更多的竞争。

2008年,人们称之为浏览器性能大战的时期开始了。很多浏览器加入了即时编译器,又称之为JITs。在这种模式下,JavaScript在运行的时候,JIT 选择模式然后基于这些模式使代码运行更快。

这些 JITs 的引入是浏览器运行代码机制的一个转折点。所有的突然之间,JavaScript 的运行速度快了10倍。
avatar

随着这种改进的性能,JavaScript 开始被用于意想不到的事情,比如使用Node.js和Electron构建应用程序。

现在 WebAssembly 可能是的另一个转折点。

avatar

在我们没有搞清楚 JavaScript 和 WebAssembly 之间的性能差前,我们需要理解 JS 引擎所做的工作。

JavaScript 是如何在浏览器中运行的呢?

作为一个开发人员,您将JavaScript添加到页面时,您有一个目标并遇到一个问题。

目标:你想要告诉计算机做什么

问题:你和计算机使用不通的语言。

您说的是人类的语言,计算机说的是机器语言。尽管你不认为 JavaScript 或者其他高级语言是人类语言,但事实就是这样的。它们的设计是为了让人们认知,不是为机器设计的。

所以JavaScript引擎的工作就是把你的人类语言转化成机器所理解的语言。

在编程中,通常有两种翻译方法将代码翻译成机器语言。你可以使用解释器或者编译器。

使用解释器,翻译的过程基本上是一行一行及时生效的。
avatar

编译器是另外一种工作方式,它在执行前翻译。
avatar

每种翻译方法都有利弊。

解释器的利弊

解释器很快的获取代码并且执行。您不需要在您可以执行代码的时候知道全部的编译步骤。因此,解释器感觉与 JavaScript 有着自然的契合。web 开发者能够立即得到反馈很重要。

这也是浏览器最开始使用 JavaScript 解释器的原因之一。

但是实用解释器的弊端是当你运行相同的代码的时候。比如,你执行了一个循环。然后你就会一遍又一遍的做同样的事情。

编译器的利弊

编译器则有相反的效果。在程序开始的时候,它可能需要稍微多一点的时间来了解整个编译的步骤。但是当运行一个循环的时候他会更快,因为他不需要重复的去翻译每一次循环里的代码。

因为解释器必须在每次循环访问时不断重新转换代码,作为一个可以摆脱解释器低效率的方法,浏览器开始将编译器引入。

不同的浏览器实现起来稍有不同,但是基本目的是相同的。他们给 JavaScript 引擎添加了一个新的部分,称为监视器(也称为分析器)。该监视器在 JavaScript 运行时监控代码,并记录代码片段运行的次数以及使用了那些数据类型。

如果相同的代码行运行了几次,这段代码被标记为 “warm”。如果运行次数比较多,就被标记为 “hot”。

被标记为 “warm” 的代码被扔给基础编译器,只能提升一点点的速度。被标记为 “hot” 的代码被扔给优化编译器,速度提升的更多。

耗时比较:JavaScript Vs. WebAssembly

这张图大致给出了现在一个程序的启动性能,目前 JIT 编译器在浏览器中很常见。

该图显示了 JS 引擎运行程序花费的时间。显示的时间并不是平均的。这个图片表明,JS 引擎做的这些任务花费的时间取决于页面中 JavaScript 做了什么事情。但是我们可以用这个图来构建一个心理模型。
avatar

每栏显示花费在特定任务上的时间。

Parsing - 将源码转换成解释器可以运行的东西所用的时间。

Compiling + optimizing - 花费在基础编译和优化编译上的时间。有一些优化编译的工作不在主线程,所以这里并不包括这些时间。

Re-optimizing - 当预先编译优化的代码不能被优化的情况下,JIT 将这些代码重新优化,如果不能重新优化那么就丢给基础编译去做。这个过程叫做重新优化。

Execution - 执行代码的过程

Garbage collection - 清理内存的时间

一个重要的事情要注意:这些任务不会发生在离散块或特定的序列中。相反,它们将被交叉执行。比如正在做一些代码解析时,还执行着一些其他的逻辑,有些代码编译完成后,引擎又做了一些解析,然后又执行了一些逻辑,等等。

这种交叉执行对早期 JavaScript 的性能有很大的帮助,早期的 JavaScript 的执行就像下图一样:
avatar

一开始,当只有一个解释器运行 JavaScript 时,执行速度相当缓慢。JITs 的引入,大大提升了执行效率。

监视和编译代码的开销是需要权衡的事情。如果 JavaScript 开发人员按照相同的方式编写JavaScript,解析和编译时间将会很小。但是,性能的提升使开发人员能够创建更大的JavaScript应用程序。

这意味着还有改进的余地。

下面是 WebAssembly 如何比较典型 web 应用。
avatar

请求

这没有展示在图上,但是从服务器获取文件是会消耗时间的

下载执行与 JavaScript 等效的 WebAssembly 文件需要更少的时间,因为它的体积更小。WebAssembly 设计的体积更小,可以以二进制形式表示。

即使使用 gzip 压缩的 JavaScript文件很小,但 WebAssembly 中的等效代码可能更小。

所以说,下载资源的时间会更少。在网速慢的情况下更能显示出效果来。

解析

JavaScript 源码一旦被下载到浏览器,源将被解析为抽象语法树(AST)。

通常浏览器解析源码是懒惰的,浏览器首先会解析他们真正需要的东西,没有及时被调用的函数只会被创建成存根。

在这个过程中,AST被转换为该 JS 引擎的中间表示(称为字节码)。

相反,WebAssembly 不需要被转换,因为它已经是字节码了。它仅仅需要被解码并确定没有任何错误。

avatar

编译 + 优化

如前所述,JavaScript 是在执行代码期间编译的。因为 JavaScript 是动态类型语言,相同的代码在多次执行中都有可能都因为代码里含有不同的类型数据被重新编译。这样会消耗时间。

相反,WebAssembly 与机器代码更接近。例如,类型是程序的一部分。这是速度更快的一个原因:

编译器不需要在运行代码时花费时间去观察代码中的数据类型,在开始编译时做优化。

编译器不需要去关注每次执行相同代码中数据类型是否一样。

更多的优化在最前面就已经完成了。所以编译和优化的工作很少。

avatar

重新优化

有时 JIT 抛出一个优化版本的代码,然后重新优化。

JIT 基于运行代码的假设不正确时,会发生这种情况。例如,当进入循环的变量与先前的迭代不同时,或者在原型链中插入新函数时,会发生重新优化。

在 WebAssembly 中,类型是明确的,因此 JIT 不需要根据运行时收集的数据对类型进行假设。这意味着它不必经过重新优化的周期。

avatar

执行

尽可能编写执行性能好的 JavaScript。所以,你可能需要知道 JIT 是如何做优化的。

然而,大多数开发者并不知道 JIT 的内部原理。即使是那些了解 JIT 内部原理的开发人员,也很难实现最佳的方案。有很多时候,人们为了使他们的代码更易于阅读(例如:将常见任务抽象为跨类型工作的函数)会阻碍编译器优化代码。

正因如此,执行 WebAssembly 代码通常更快。有些必须对 JavaScript 做的优化不需要用在 WebAssembly 上

另外,WebAssembly 是为编译器设计的。意思是,它是专门给编译器来阅读,并不是当做编程语言让程序员去写的。

由于程序员不需要直接编程,WebAssembly 提供了一组更适合机器的指令。根据您的代码所做的工作,这些指令的运行速度可以在10%到800%之间。

avatar

垃圾回收

在 JavaScript 中,开发者不需要担心内存中无用变量的回收。JS 引擎使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。

这对于控制性能可能并不是一件好事。你并不能控制垃圾回收时机,所以它可能在非常重要的时间去工作,从而影响性能。

现在,WebAssembly 根本不支持垃圾回收。内存是手动管理的(就像 C/C++)。虽然这些可能让开发者编程更困难,但它的确提升了性能。

avatar

总而言之,这些都是在许多情况下,在执行相同任务时WebAssembly 将胜过 JavaScript 的原因。

WebAssembly 是如何工作的?

编译器将代码翻译成汇编代码(符号机器码)。
每种机器内部有不同的结构,所以每种机器都有自己独有的汇编语言。

avatar

这样做的效率非常低。为了解决这个问题,大多数编译器会在高级语言和汇编语言之间多加一层。编译器将把高级语言翻译成一种更低级的语言,但比机器码的等级高。这就是中间代码(IR)。

avatar

意思就是编译器可以将任何一种高级语言转换成一种中间语言。然后,编译器的另外的部分将中间语言编译成目标机器的汇编代码。

编译器的“前端”将高级编程语言转换为IR。编译器的“后端”将 IR 转换成目标机器的汇编代码。

负责思考的部分是算术逻辑单元(ALU)。

短期储存由寄存器(Registers)提供。

随机存储器(或RAM)来提供长期储存能力。

avatar

如果想在页面里上添加 WebAssembly,需要将代码编译成 .wasm 文件。

编译到 .wasm 文件

当前对 WebAssembly 支持最多的编译器工具链称是 LLVM。有许多不同的“前端”和“后端”可以插入到 LLVM 中。

注意:大多数 WebAssembly 模块开发者使用 C 和 Rust 编写代码,然后编译成 WebAssembly,但是这里有其他创建 WebAssembly 模块的途径。比如,这里有一个实验性工具,他可以帮你使用 Type 创建一个 WebAssembly 模块,你可以在这里直接编辑WebAssembly。

架设我们想通过 C 来创建 WebAssembly。我们可以使用 clang “前端” 从 C 编译成 LLVM 中间代码。当它变成 LLVM 的中间代码(IR)以后,LLVM 可以理解他,所以 LLVM 可以对代码做一些优化。

如果想让 LLVM 的 IR 变成 WebAssembly,我们需要一个 “后端”。目前 LLVM 项目中有一个正在开发中的。这个“后端”对做这件事情很重要,应该很快就会完成。可惜,它现在还不能用。

另外有一个工具叫做 Emen,它用起来比较简单。它还可以有比较有用的可以选择,比如说由 IndexDB 支持的文件系统。

avatar

引用复杂,数据传输费劲,双方api调用麻烦,开发体验差(个人体会)。

现状

avatar

总结

使用WebAssembly,可以更快地在 web 应用上运行代码。这里有 几个 WebAssembly 代码运行速度比 JavaScript 高效的原因。

文件加载 - WebAssembly 文件体积更小,所以下载速度更快。

解析 - 解码 WebAssembly 比解析 JavaScript 要快

编译和优化 - 编译和优化所需的时间较少,因为在将文件推送到服务器之前已经进行了更多优化,JavaScript 需要为动态类型多次编译代码

重新优化 - WebAssembly 代码不需要重新优化,因为编译器有足够的信息可以在第一次运行时获得正确的代码

执行 - 执行可以更快,WebAssembly 指令更接近机器码

垃圾回收 - 目前 WebAssembly 不直接支持垃圾回收,垃圾回收都是手动控制的,所以比自动垃圾回收效率更高。

目前浏览器中的 MVP(最小化可行产品) 已经很快了。在接下来的几年里,随着浏览器的发展和新功能的增加,它将在未来几年内变得更快。没有人可以肯定地说,这些性能改进可以实现什么样的应用。但是,如果过去有任何迹象,我们可以期待惊奇。

性能对比 js WebAssembly todo

引用几张图让你看懂WebAssembly
引用WebAssembly中文网

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 背景介绍
  2. 2. js性能历史
  3. 3. JavaScript 是如何在浏览器中运行的呢?
    1. 3.1. 解释器的利弊
    2. 3.2. 编译器的利弊
  4. 4. 耗时比较:JavaScript Vs. WebAssembly
    1. 4.1. 请求
    2. 4.2. 解析
    3. 4.3. 编译 + 优化
    4. 4.4. 重新优化
    5. 4.5. 执行
    6. 4.6. 垃圾回收
  5. 5. WebAssembly 是如何工作的?
  6. 6. 编译到 .wasm 文件
  7. 7. 现状
  8. 8. 总结
  9. 9. 性能对比 js WebAssembly todo
,