从 JS 引擎谈到 WebAssembly
Posted 腾讯AlloyTeam
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从 JS 引擎谈到 WebAssembly相关的知识,希望对你有一定的参考价值。
最近团队技术分享会有提及到JS引擎的相关的知识,自己就总结了一下,又从JS引擎扩展到了最近比较火的新技术webAssembly。
一. JS 引擎
1. 粗略的介绍 JS 引擎的工作内容
从一段 javascript 代码开始。JavaScript 引擎会解析码源并将其转化成抽象语法树(AST)。基于 AST,解释器(interpreter)会进一步地生 成字节码。当然这只是 JS 引擎大概的一个方案,真正 JS 的引擎是有进行优化的。
补充:介绍编译器与解释器
解释器:
解释器启动和执行的更快。你不需要等待整个编译的过程就可以完成你的代码。从第一行就开始翻译,就可以就可以依次的继续执行了。试想想,对于 web 工作者,能够更快的去执行部分代码,让用户看着见是更重要的事情。所以 javaScript 更适合解释器。但是同样弊端也出现了。对一些重复性的代码,比如循环,那么解释器需要一遍一遍的去进行翻译,很显然这是他的劣势。
编译器:
编译器正好与解释器相反,他可能需要长时间花费的时间是在对整个码源进行编译,然后生成可以在机器上可以执行的代码,因为是事先编译好的,所以对于循环来说不需要额外的花费。
而且编译型语言可以在编译是就去进行代码的优化处理,而解释型语言需要在运行时,所以编译型代码执行速度更加的快。
2. JS 引擎中的编译器
这些年各大互联网公司对于 JS 引擎的优化可谓进入了白热化,JS 引擎早已不是简单的解释器了。本身就是 JS 就是解释型的语言,如何加入符合策略的编译器则成为了优化的方向。下面是几种应用与 JS 引擎中的优化编辑器。
Just-in-time 编译器
综合了解释器与编译器的工作原理,Just-in-time 编译器综合了两者的优点。但是他如何在解释器中增加编辑器的呢?其实是在 JS 引擎中新增了一个分析器。这个分析器的主要作用是分析了代码运行的情况,记录代码运行次数,如何运行等信息的。会有不同的状态去标记。运行了几次会有"warm",运行了多次会有"hot"状态。
基线编辑器
当然只有编辑是不行的,对于重复执行代码,JIT 会把它送去编译器,并把编译结果储存起来(行号+变量类型)。如果监听器执行了相同的代码,并是相同类型的变量,那么就直接将编译好的代码推出。这样就大大提升了引擎的速度。
优化编辑器
这里对使用及其频繁的代码又进一步的优化,对于使用真的特别多的代码,那它的耗时也是最多的,优化编辑器会生成一个更高效的版本,是一个有一些预期假设的版本。比如一个构造函数生成的所有实例都有相同的属性,属性的顺序都是一致的。但是如果某一次的属性不一致了,则 JIT 会认为这个是一个错误的假设,因此就会把优化的代码去掉,这是执行过程就是回到基线编辑器或者是解释器中,那么频繁的切换势必会造成性能上的不足,因此大部分浏览器对优化编辑器做了优化次数的限制。
3. 以 V8 引擎概括整个流程
V8 的引擎解释器叫做 lgnition,主要负责生成和执行字节码。当字节码运行时,解释器会有分析器去分析收集,这些数据之后可能会被用来提升速度而被缓存。如果一个函数或者代码块被经常的调用,即 hot,那么,经过解释器转换来的字节码和收集到的分析数据会传给 TurboFan(V8 的优化编译器),进一步被加工成高度优化的机器码。
补充:JS Runtime 和 JS 引擎的关系
JS Runtime 更像是一个原生的项目,他将 JS 引擎视为专用的解释器,为其提供了操作系统的网络、进程、文件系统等平台能力。简单的说,JS 引擎在 JS Runtime 中运行,JS runtime 为引擎提供了一些内建的库,可以在程序运行时使用。
Window 对象和 DOM API, 都存在于浏览器的 Runtime 而不存在于 Node Runtime;相反,Cluster 和 FileSystem API 存在于 Node Runtime 却不存在于浏览器 Runtime。当然,两个 Runtime 都包含内置的数据类型和常用的工具,比如 Console 对象,定时器。
因此 Chrome 和 Node.js 共享相同的引擎(V8),但是他们有不同的 runtime。
二. WebAssembly
有了上面 JS 引擎的铺垫,可以看出,虽然引擎是做了很多编译上的优化,特别是 V8 引擎将 web 的性能提升了 10 倍,但是速度始终没有直接编译,运行的速度要快。因此大家开始将优化方向的目光转向了另一个方向,如果 JavaScript 也拥有强类型,那么在岂不是可以告诉编辑器变量的准确类型,从而提升了 JIT 性能?因此 asm.js 诞生了。
1. Webassembly 的前身 asm.js
和 TypeScript 相似,asm.js 也是拥有强类型的 JavaScript,但是他的语法是 JavaScript 的子集,他是为了提升 JIT 性能而专门打造的。虽然 asm.js 对于静态类型的问题解决的很好,但是他始终逃不过 parser 和 ByteCode Compiler ,这个也是对 JS 代码比较耗时的两部分。
function add1(x) {
;
x = x | 0;
return (x + 1) | 0;
}
使用了注释,检测等方式来保证强类型。
2. 什么是 WebAssembly
asm.js 的思路,得到了一众浏览器厂商的支持,因此一同创建了 WebAssembly 生态。什么 WebAssembly?官方的解释是 WebAssembly(wasm)就是一个可移植、体积小、加载快而且兼容 Web 的全新格式。其实 WebAssembly 就是一份字节码标准,以字节码的形式依赖虚拟机在浏览器中运行。
因为 WebAssembly 的天然优势,编译后的二进制代码无需经过 parser 和 compiler 两个步骤,完全绕过 JS,在面对一些高计算量、对性能要求高的应用场景图像/视频解码、图像处理、3D 等的优势十分明显。
3. WebAssembly 的使用
因为这一部分包括环境安装和运行在各大平台的安装方法,官方文档说明的很清楚,所以这里不去进行多余的描述。
可以参考官方文档,这里提几个值得注意的地方。
(1) EMSCRIPTEN_KEEPALIVE
默认情况下,Emscripten 生成的代码只会去调用 main()函数,其他的函数被视为无用的代码。在之前添加 EMSCRIPTENKEEPALIVE 能够防止这样的事情发生。你需要导入 emscripten.h 库来使用 EMSCRIPTENKEEPALIVE。
(2) 编译成.wasm 和 .html
编译成.wasm 需要有载入的操纵,通过 fetch 取得二进制字节流后,将其转为 ArrayBuffer,利用 WebAssembly.compile 来产生 WebAssembly 模块,接着通过 WebAssembly.instantiate 来产生模块实例,最后的 instance.exports 就是我们在 wasm 中导出出来的物件或函数。
生成 html 则不需要,或生成对应的 js,这个 js 叫做胶水代码,这样我们可以直接在 web 环境中去运行,但是生成的胶水文件体积很大,在某些情况下可能会影响性能。
(3) 在 WebAssembly 中使用 js
C 代码
void jsLog(int num);
intadd(int num1, int num2){
int r = num1 + num2;
jsLog(r);
return r;
}
函数链接
fetchAndInstantiateWasm('./add.wasm', {
env: {
jsLog: num => console.log(num)
}
})
.then(m => {
m.add(5,3) // 打开console.log看到输出8
})
4. WebAssembly 性能测试
首先我们对斐波那契数列的算法进行.wasm 与.js 进行运行时间的比较: c 代码:
int fib(int a,int b) {
return n <= 1
? 1
: fib(n - 1) + fib(n - 2);
}
js 代码:
function jsfib(a, b) {
return n <= 1 ? 1 : jsfib(n - 1) + jsfib(n - 2);
}
运行时间对比:
根据数据得处的结论是随着 n 的数量变大.js 的运行时间差不多.wasm 的 2 倍,而且随着时间的 n 的增大,有大于 2 倍的趋势。
但是这个结果去告诉我们所有的代码都应该去切换成.wasm 去执行吗?答案当然是否定了,测试的过程中可以完全发现.wasm 的启动速度要比.js 慢的多。下面我们来对简单求和在做一个测试。
C 代码:
int add(int a,int b) {
return a+b;
}
JS 代码:
function jsadd(a, b) {
return a + b;
}
运行时间对比:
虽然测试的案例过于极端,但是通过测试结果也可以发现在加分运算中.wasm 与.js 的性能几乎相差无几,因为 JIT 已经给我们去进行了优化。
5. WebAssembly 的目前的现状
目前 WASM 是不支持 GC,可想而知如果 WASM 加上 GC 之后,会不会有与 JS 对象的生命周期处理起来一样的复杂呢?这些确实值得 Google 去来操心了。
线程的支持,异常处理,尾调用优化等都需要时间来优化。
兼容性
6. 结论
根据上面的数据显示,我们在选择技术方案的时候,切勿觉得 JS 慢就像靠 WASM 来嵌入 C,其实 JS 引擎发展到现在的这个阶段,已经很大程度上帮助了我们去更快的运行 JS 了,虽然 JavaScript 跟 C、C++等静态语言相比执行速度还有很大差距,但是大多数 Web 应用的性能瓶颈已经不是 JavaScript 语言本身了,反而是网络资源的加载,这一点 WebAssembly 并无优势。
引用团队大佬对 WebAssembly 的总结
WebAssembly 本质上仅仅是做了 golangc++的 front end 解析,拿到了语法树,直接变成类似 java 的虚拟机字节码。就是多语言 front-end,自己统一一个 back-end。一个语言的性能,核心还在于 back-end 的优化。也就是编译原理主要解决的问题,把很多逻辑对等的逻辑,使用一个更高效的逻辑。换句话说,js 可以看作字节码,有 jit,WebAssembly 也是字节码,也要 jit。谁都不能比谁强。WebAssembly 锁死强类型,倒是可以做内敛优化、内存布局优化、无 gc 等等优化,但是空间也不是很大。所以 WebAssembly 复用别的语言代码的代码生态的价值,前后端代码统一的价值(DRY),大于性能价值。汇编、C 什么都能做。
所谓高级语言,本质是限定程序员不能做很多事情,只能用有限的固定的范式实现同一个逻辑。来降低这个语言表达的复杂度,进而建立庞大的代码复用生态。c++ ust 的性能,主要来自低 runtime,做任何事儿都要程序员自己去写,管理内存,管理并发。做事情变得困难,生态无法统一。golang、java、js 这些大 runtime,降低了语言表达的难度,限制特别多,生态一般比较强。限制多就更统一。 如果没有前后端统一的需求,抛弃生态强大的 js,转投 c++、rust + WebAssembly,那简直神经病。golang 的生态还行。js 的正则是大量使用回溯的,不碰到正则回溯的坑,js 性能不错。ts 也是限制 js,结果 less is more。
三. 启示
之前的社区出现了一种声音,未来 WebAssembly 可以完全替代 JavaScript 的声音,其实目前来看还为时尚早,目前只是因为我们对于网页的性能要求越来越高,要在网页上进行大数据的计算,要去运行高质量的游戏,甚至要去渲染 VR/AR,WebAssembly 绝对是一个可行的解决方案。
除了 Firefox,Google 也在其 Chrome 浏览器和 Chromium 项目中拥抱了 WebAssembly,所以作为一个优秀的前端开发,我们应该对 WebAssembly 引起足够的重视,未来快速加载 Web 应用程序的需求肯定是必须的。
关于AlloyTeam
AlloyTeam 是国内影响力最大的前端团队之一,核心成员来自前 WebQQ 前端团队。 AlloyTeam负责过WebQQ、QQ群、兴趣部落、腾讯文档等大型Web项目,积累了许多丰富宝贵的Web开发经验。
这里技术氛围好,领导nice、钱景好,无论你是身经百战的资深工程师,还是即将从学校步入社会的新人,只要你热爱挑战,希望前端技术和我们飞速提高,这里将是最适合你的地方。
加入我们,请将简历发送至 alloyteam@qq.com,或直接在公众号留言~
期待您的回复 以上是关于从 JS 引擎谈到 WebAssembly的主要内容,如果未能解决你的问题,请参考以下文章
如何从 WebAssembly 函数返回 JavaScript 字符串