WebAssembly
是一种可以使用非 JavaScript
编程语言编写代码并且能在浏览器上运行的技术方案。
解释器与编译器
编程中,通常有两种翻译方法将代码翻译成机器语言:解释器、编译器。
-
使用解释器,翻译的过程基本上是一行一行及时生效的;
-
编译器是另外一种工作方式,它在执行前翻译。
- 每种翻译方法都有利弊
- 解释器很快的获取代码并且执行。不需要在可以执行代码的时候知道全部的编译步骤。因此,解释器感觉与 JavaScript 有着自然的契合。web开发者能够立即得到反馈很重要。
这也是浏览器最开始使用 JavaScript 解释器的原因之一。
但使用解释器的弊端是,当运行相同的代码时,如执行一个循环。那就会一遍又一遍的做同样的事情! - 编译器则有相反的效果。在程序开始执行时,它可能需要稍微多一点的时间来了解整个编译的步骤,但当运行一个循环时会更快,因为它不需要重复地去翻译每一次循环里的代码。
- 作为一个可以摆脱解释器低效率的方法,浏览器开始引入编译器。浏览器给
JS引擎
添加了一个新的部分--
监视器(分析器),在JS运行时监控代码,并记录代码片段运行的次数,以及使用的数据类型;- 如果相同的代码块运行了几次,则被标记为
warm
。如果运行次数比较多,就被标记为hot
。 -
warm
代码块被扔给基础编译器,只能提升一点点的速度。hot
代码块则被扔给优化编译器,速度大大提升。
- 如果相同的代码块运行了几次,则被标记为
性能瓶颈
要了解WebAssembly
,首先要了解JS引擎的工作原理 ---
明确一点,JS没有强制类型约束!
-
JavaScript
文件会被下载下来 - 然后进入
Parser
,Parser
会把代码转化成AST(抽象语法树)
- 然后根据抽象语法树,
Bytecode Compiler
字节码编译器会生成引擎能够直接阅读、执行的字节码 - 字节码进入翻译器,将字节码一行一行的翻译成效率十分高的
Machine Code
在项目运行的过程中,引擎会对执行次数较多的
function
记性优化,其代码将编译成Machine Code
后打包送到顶部的Just-In-Time(JIT) Compiler(即时编译器)
,下次再执行这个function
,就会直接执行编译好的Machine Code
。但是由于JavaScript
的动态变量,上一秒可能是Array
,下一秒就变成了Object
。那么上一次引擎所做的优化,就失去了作用,此时又要再一次进行优化。
asm.js
为了解决这个问题,
WebAssembly
的前身,asm.js
诞生了。asm.js
是一个Javascript
的严格子集,合理合法的asm.js
代码一定是合理合法的JavaScript
代码,反之就不成立。同WebAssembly
一样,asm.js
不是用来一行一行写代码的,asm.js
是一个编译目标。它的可读性、可读性虽然比WebAssembly
好,但对开发者来说,仍然是无法接受的。
asm.js 的静态类型约束
function asmJs() {
'use asm';
let myInt = 0 | 0;
let myDouble = +1.1;
}
看似问题解决了,但不管
asm.js
对静态类型做得再好,它始终逃不过Parser --> ByteCode Compiler
,它们是JavaScript
代码在引擎执行过程当中消耗时间最多的两步。而WebAssembly
不用经过这两步。这就是WebAssembly
比asm.js
更快的原因。
WebAssembly
在2015年,
WebAssembly
横空出世。WebAssembly
是经过编译器编译之后的代码,体积小、起步快。在语法上完全脱离JavaScript
,同时具有沙盒化的执行环境。WebAssembly
同样的强制静态类型,是C/C++/Rust的编译目标。
JavaScript vs WebAssembly
- 目前
JIT
编译器在浏览器中很常见,JS引擎运行一个程序花费的时间
-
Parse
源码转换成解释器可以运行的代码; -
Compiling + optimizing
花费在基础编译和优化编译上的时间。有一些优化编译的工作不在主线程,这里不包括这些时间; -
Re-optimizing
当预先编译优化的代码不能被优化的情况下,JIT
将这些代码重新优化,如果不能重新优化,则丢给基础编译去做,这个过程叫做重新优化; -
Execution
执行代码的过程; -
Garbage Collection(GC)
清理内存的时间。
- 运行一个
WebAssembly
程序花费的时间
-
request -> download
图上面并没有展示从服务器上下载所消耗的时间,WebAssembly
设计的体积更小,可以以二进制形式表示,所以下载执行与JavaScript
等效的WebAssembly
文件需要更少的时间;
即使使用gzip
压缩的JavaScript
文件很小,但WebAssembly
中的等效代码可能更小,在网速慢的情况下更能显示出效果来。 -
Parse -> decode
JS源码一旦被下载到浏览器,将被解析为抽象语法树(AST);通常浏览器解析源码是惰性的,浏览器首先会解析它们真正需要的东西,没有及时被调用的函数只会被创建成存根。
在这个过程中,AST
被转换为该 JS 引擎的中间表示(称为字节码)。
相反,WebAssembly
不需要被转换(Parse)
,因为它已经是目标代码(字节码)了,仅仅需要被解码(decode)
并确定没有任何错误。 -
Compiling + optimizing
如前所述,JavaScript
是在执行代码期间编译的。因为JavaScript
是动态类型语言,相同的代码在多次执行中都有可能都因为代码里含有不同的类型数据被重新编译,这样会消耗时间。
而WebAssembly
与机器代码更接近,编译器不需要在运行代码时花费时间去观察代码中的数据类型,在开始编译时做优化,更多的优化在LLVM
最前面就已经完成了,所以编译和优化的工作很少。 -
Re-optimizing
有时JIT
抛出一个优化版本的代码,然后重新优化。JIT 基于运行代码的假设不正确时,会发生这种情况。例如,当进入循环的变量与先前的迭代不同时,或者在原型链中插入新函数时,会发生重新优化。
在WebAssembly
中,类型是明确的,因此JIT
不需要根据运行时收集的数据对类型进行假设。也就是说,WebAssembly
不需要重新优化的周期。 -
Execution
想要编写执行性能好的JavaScript
,就需要知道JIT
是如何做优化的;然而大多数开发者并不知道JIT
的内部原理,即使是那些了解JIT
内部原理的开发人员,也很难实现最佳方案。有很多时候,开发者为了使他们的代码更易于阅读会阻碍编译器优化代码。
也正因如此,执行WebAssembly
代码通常更快,有些必须对JavaScript
做的优化不需要用在WebAssembly
上。另外,WebAssembly
是为编译器设计的,它是目标程序,专门给编译器来阅读,并不是当做编程语言让程序员去写的。
由于程序员不需要直接编程,WebAssembly
提供了一组更适合机器的指令,根据程序代码所做的工作,这些指令的运行速度可以在10%
到800%
之间。 -
GC
在 JavaScript 中,JS 引擎使用垃圾回收器来自动进行垃圾回收处理,这对于控制性能可能并不是一件好事。开发者不能控制垃圾回收的时机,所以它可能在非常重要的时间去工作,从而影响性能。
WebAssembly
根本不支持垃圾回收,内存是手动管理的(就像 C/C++)
,虽然可能让编程更困难,但确实提升了性能。 - 总而言之,这些都是在许多情况下,在执行相同任务时
WebAssembly
将胜过JavaScript
的原因。
甚至在某些情况下,WebAssembly 不能像预期的那样执行,还有一些更改使其更快。
WebAssembly如何工作
- 不同的机器架构有自己独特的汇编语言,也就是说每一种汇编语言都对应特性的机器架构;
- 我们也可以把
WebAssembly
当做是另外一种目标汇编语言,当我们的代码运行在用户机器的 web 平台上时,我们根本不可能知道用户机器的架构。
WebAssembly
与别的汇编语言不同,它是一个概念机上的机器语言,而不是在一个真正存在的物理机上运行的机器语言,这一点非常重要!
正因如此,WebAssembly
指令有时又被称为虚拟指令,它比JavaScript
代码更快更直接的转换成机器代码,但又不直接与特定硬件的特定机器代码对应。 -
WebAssembly
被编译到.wasm
文件,在浏览器下载后,能迅速转换成目标机器的汇编代码。
使用场景
- 对性能有很高要求的
App/Module/游戏
- 在Web中调用
C/C++/Rust/Go
的库
开发工具
-
AssemblyScript 支持直接将
TypeScript
编译成WebAssembly
,入门的门槛低。 -
Emscripten 可以说是
WebAssembly的
灵魂工具,上面说了很多编译,这个就是那个编译器,将其他的高级语言,编译成WebAssembly
。 -
WABT 是个将
WebAssembly
在字节码和文本格式相互转换的一个工具,方便开发者去理解wasm
到底是在做什么事。