WebAssembly
可以让C/C++、Rust、Java、C#等多种高级语言编写的代码,在Web上以接近原生性能的方式运行,有越来越多的大型应用通过它跑在了网页上,例如:AutoCAD在线版、Figma在线版,还有在微信上支持运行Unity小游戏,底层的关键技术都是基于WebAssembly。
要了解WebAssembly
,先从JavaScript
的执行流程说起。
一 JavaScript的执行流程
1.1 编译型语言和解释型语言
编程语言按执行流程划分,可以分为编译型和解释型。
编译型语言在执行前,需要经过编译器的编译,生成机器能读懂的二进制文件,这样每次就直接运行改文件,而无需重新编译。例如C/C++、GO、Rust等都是编译型语言。
解释型语言则不需要事先编译,每次运行时由解释器对程序进行动态解释和执行。例如JavaScript、Python等都是解释型语言。
它们的执行流程如下。
从图中可以看出,在JavaScript
的执行过程中,JavaScript引擎会先对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后再根据抽象语法树生成字节码,最后根据字节码来执行程序、输出结果。
1.2 抽象语法树(简称AST)
AST
是非常重要的一种数据结构,因为高级语言是开发者使用的语言,但对于编译器或解释器来说,它们可以理解的就是AST。
例如广泛使用的Babel
,它可以将ES6代码转为ES5代码。工作原理就是先将ES6源码转为AST,然后将ES6语法的AST转为ES5语法的AST,最后生成符合ES5规范的JavaScript源代码。借助于AST,无论多么复杂的源代码,只要能正确生成AST,就能正确的转换。
无论使用哪种编程语言的代码,在加载到内存后都需要先转成AST,才能执行进一步的编译或解释。
1.3 字节码
字节码是由JavaScript引擎定义的一种代码,介于AST和机器码之间,与特定机器无关,不能直接运行,需要解释器将其转换为机器码后才能执行。
以广泛使用的JavaScript引擎V8为例,最早时候没有字节码,直接将AST转换为机器码,执行效率非常好,但需要大量内存来存放转换后的机器码。随着移动设备的普及,内存占用问题很严重,V8引擎不得不进行大幅重构,引入了字节码,因为字节码的占用空间远远小于机器码,所以大大减轻了内存问题。
1.4 执行
JavaScript引擎的解释器除了生成字节码,还负责逐条解释执行。在很久前,即使一段代码是反复执行的,解释器仍然不断重复地进行逐条解释执行的流程,所以JavaScript的执行效率一直不高。直到2008年,Google在V8引擎中率先使用JIT
技术,把JavaScript的执行性能提升了10倍,催生出如Node.js
这种在后端也能用JavaScript的框架,直接推动了JavaScript语言一统前后端的趋势。
JIT
机制是这样:在执行过程中,如果发现一段代码被重复执行多次,这种称为热点代码
,V8引擎中的编译器
就会把该段字节码编译为高效的机器码,并缓存起来,然后当再次执行这段代码时,就不需要逐条解释,直接执行编译后的机器码就可以了,从而大大提升了执行效率。这种技术就称为即时编译(JIT)
。
执行流程如下图:
二 总结
除了Google的V8,其它的主流JS引擎,如:Apple 的 JavaScriptCore、Mozilla的 SpiderMonkey 都采用了相似的JIT
技术,这使得JS执行效率普遍得到了飞跃式的提升。
但JIT
技术并不是万能的,因为它需要额外缓存热点代码
对应的机器码,所以开启了JIT
机制的JS引擎占用内存仍然很大,如Chrome、Electron应用等都是内存消耗大户。
另外在iOS设备上,苹果公司出于安全考虑,从系统层面禁止了内存页中数据段的执行权限,从根本上限制了JIT的使用,因此iOS设备上的JS引擎无法使用JIT
,导致iOS系统上JS的执行效率远低于安卓系统。据测试,没有JIT的V8比启用JIT的V8慢了约5-15倍。