从 JS 引擎谈到 WebAssembly

Posted 腾讯AlloyTeam

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从 JS 引擎谈到 WebAssembly相关的知识,希望对你有一定的参考价值。

最近团队技术分享会有提及到JS引擎的相关的知识,自己就总结了一下,又从JS引擎扩展到了最近比较火的新技术webAssembly。

一. JS 引擎

1. 粗略的介绍 JS 引擎的工作内容

从一段 javascript 代码开始。JavaScript 引擎会解析码源并将其转化成抽象语法树(AST)。基于 AST,解释器(interpreter)会进一步地生 成字节码。当然这只是 JS 引擎大概的一个方案,真正 JS 的引擎是有进行优化的。

补充:介绍编译器与解释器

解释器:

解释器启动和执行的更快。你不需要等待整个编译的过程就可以完成你的代码。从第一行就开始翻译,就可以就可以依次的继续执行了。试想想,对于 web 工作者,能够更快的去执行部分代码,让用户看着见是更重要的事情。所以 javaScript 更适合解释器。但是同样弊端也出现了。对一些重复性的代码,比如循环,那么解释器需要一遍一遍的去进行翻译,很显然这是他的劣势。

编译器:

编译器正好与解释器相反,他可能需要长时间花费的时间是在对整个码源进行编译,然后生成可以在机器上可以执行的代码,因为是事先编译好的,所以对于循环来说不需要额外的花费。

从 JS 引擎谈到 WebAssembly

而且编译型语言可以在编译是就去进行代码的优化处理,而解释型语言需要在运行时,所以编译型代码执行速度更加的快。

从 JS 引擎谈到 WebAssembly

2. JS 引擎中的编译器

这些年各大互联网公司对于 JS 引擎的优化可谓进入了白热化,JS 引擎早已不是简单的解释器了。本身就是 JS 就是解释型的语言,如何加入符合策略的编译器则成为了优化的方向。下面是几种应用与 JS 引擎中的优化编辑器。

Just-in-time 编译器

综合了解释器与编译器的工作原理,Just-in-time 编译器综合了两者的优点。但是他如何在解释器中增加编辑器的呢?其实是在 JS 引擎中新增了一个分析器。这个分析器的主要作用是分析了代码运行的情况,记录代码运行次数,如何运行等信息的。会有不同的状态去标记。运行了几次会有"warm",运行了多次会有"hot"状态。

基线编辑器

当然只有编辑是不行的,对于重复执行代码,JIT 会把它送去编译器,并把编译结果储存起来(行号+变量类型)。如果监听器执行了相同的代码,并是相同类型的变量,那么就直接将编译好的代码推出。这样就大大提升了引擎的速度。

优化编辑器

这里对使用及其频繁的代码又进一步的优化,对于使用真的特别多的代码,那它的耗时也是最多的,优化编辑器会生成一个更高效的版本,是一个有一些预期假设的版本。比如一个构造函数生成的所有实例都有相同的属性,属性的顺序都是一致的。但是如果某一次的属性不一致了,则 JIT 会认为这个是一个错误的假设,因此就会把优化的代码去掉,这是执行过程就是回到基线编辑器或者是解释器中,那么频繁的切换势必会造成性能上的不足,因此大部分浏览器对优化编辑器做了优化次数的限制。

从 JS 引擎谈到 WebAssembly

3. 以 V8 引擎概括整个流程

V8 的引擎解释器叫做 lgnition,主要负责生成和执行字节码。当字节码运行时,解释器会有分析器去分析收集,这些数据之后可能会被用来提升速度而被缓存。如果一个函数或者代码块被经常的调用,即 hot,那么,经过解释器转换来的字节码和收集到的分析数据会传给 TurboFan(V8 的优化编译器),进一步被加工成高度优化的机器码。

从 JS 引擎谈到 WebAssembly

补充: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。

从 JS 引擎谈到 WebAssembly

二. 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) { "use asm"; x = x | 0; return (x + 1) | 0;}

使用了注释,检测等方式来保证强类型。

2. 什么是 WebAssembly

asm.js 的思路,得到了一众浏览器厂商的支持,因此一同创建了 WebAssembly 生态。什么 WebAssembly?官方的解释是 WebAssembly(wasm)就是一个可移植、体积小、加载快而且兼容 Web 的全新格式。其实 WebAssembly 就是一份字节码标准,以字节码的形式依赖虚拟机在浏览器中运行。

从 JS 引擎谈到 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 代码

#include<math.h>
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 代码:

#include <stdio.h>
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);}

运行时间对比:

从 JS 引擎谈到 WebAssembly

根据数据得处的结论是随着 n 的数量变大.js 的运行时间差不多.wasm 的 2 倍,而且随着时间的 n 的增大,有大于 2 倍的趋势。

但是这个结果去告诉我们所有的代码都应该去切换成.wasm 去执行吗?答案当然是否定了,测试的过程中可以完全发现.wasm 的启动速度要比.js 慢的多。下面我们来对简单求和在做一个测试。

C 代码:

#include <stdio.h>
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 字符串

从 ASM.JS 到 WebAssembly

从0开发3D引擎:函数式反应式编程及其在引擎中的应用Web 3D是否需要WebAssembly?

WebAssembly 开启微服务新时代

送5本书WebAssembly及其 API 的完整介绍

从 JavaScript 终止 WebAssembly