JavaScript深刻浅出第4课:V8引擎是如何工做的?

JavaScript深刻浅出》系列javascript

最近,JavaScript生态系统又多了2个很是硬核的项目。java

大神Fabrice Bellard发布了一个新的JS引擎QuickJS,能够将JavaScript源码转换为C语言代码,而后再使用系统编译器(gcc或者clang)生成可执行文件。node

Facebook为React Native开发了新的JS引擎Hermes,用于优化安卓端的性能。它能够在构建APP的时候将JavaScript源码编译为Bytecode,从而减小APK大小、减小内存使用,提升APP启动速度。git

做为JavaScript程序员,只有极少数人有机会和能力去实现一个JS引擎,可是理解JS引擎仍是颇有必要的。本文将介绍一下V8引擎的原理,但愿能够给你们一些帮助。程序员

JavaScript引擎

咱们写的JavaScript代码直接交给浏览器或者Node执行时,底层的CPU是不认识的,也无法执行。CPU只认识本身的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情,好比,咱们要计算N阶乘的话,只须要7行的递归函数:github

function factorial(N) {
    if (N === 1) {
        return 1;
    } else {
        return N * factorial(N - 1);
    }
}
复制代码

代码逻辑也很是清晰,与阶乘的数学定义完美吻合,哪怕不会写代码的人也能看懂。算法

可是,若是使用汇编语言来写N阶乘的话,要300+行代码n-factorial.s编程

这个N阶乘的汇编代码是我大学时期写的,已是N年前的事情了,它须要处理10进制与2进制的转换,须要使用多个字节保存大整数,最多能够计算大概500左右的N阶乘。小程序

还有一点,不一样类型的CPU的指令集是不同的,那就意味着得给每一种CPU重写汇编代码,这就很崩溃了。。。后端

还好,JavaScirpt引擎能够将JS代码编译为不一样CPU(Intel, ARM以及MIPS等)对应的汇编代码,这样咱们才不要去翻阅每一个CPU的指令集手册。固然,JavaScript引擎的工做也不仅是编译代码,它还要负责执行代码、分配内存以及垃圾回收

虽然浏览器很是多,可是主流的JavaScirpt引擎其实不多,毕竟开发一个JavaScript引擎是一件很是复杂的事情。比较出名的JS引擎有这些:

还有,最近发布QuickJSHermes也是JS引擎,它们都超越了浏览器范畴,Atwood's Law再次获得了证实:

Any application that can be written in JavaScript, will eventually be written in JavaScript.

V8:强大的JavaScript引擎

在为数很少JavaScript引擎中,V8无疑是最流行的,Chrome与Node.js都使用了V8引擎,Chrome的市场占有率高达60%,而Node.js是JS后端编程的事实标准。国内的众多浏览器,其实都是基于Chromium浏览器开发,而Chromium至关于开源版本的Chrome,天然也是基于V8引擎的。神奇的是,就连浏览器界的独树一帜的Microsoft也投靠了Chromium阵营。另外,Electron是基于Node.js与Chromium开发桌面应用,也是基于V8的。

V8引擎是2008年发布的,它的命名灵感来自超级性能车的V8引擎,勇于这样命名确实须要一些实力,它性能确实一直在稳步提升,下面是使用Speedometer benchmark的测试结果:

图片来源:v8.dev/

V8在工业界已经很是成功了,同时它还得到了学术界的确定,拿到了ACM SIGPLAN的Programming Languages Software Award

V8's success is in large part due to the efficient machine code it generates. Because JavaScript is a highly dynamic object-oriented language, many experts believed that this level of performance could not be achieved. V8's performance breakthrough has had a major impact on the adoption of JavaScript, which is nowadays used on the browser, the server, and probably tomorrow on the small devices of the internet-of-things.

JavaScript是一门动态类型语言,这会给编译器增长很大难度,所以专家们以为它的性能很难提升,可是V8竟然作到了,生成了很是高效的machine code(实际上是汇编代码),这使得JS能够应用在各个领域,好比Web、APP、桌面端、服务端以及IOT。

严格来说,V8所生成的代码是汇编代码而非机器代码,可是V8相关的文档、博客以及其余资料都把V8生成的代码称做machine code。汇编代码与机器代码不少是一一对应的,也很容易互相转换,这也是反编译的原理,所以他们把V8生成的代码称为Machine Code也何尝不可,可是并不严谨。

V8引擎的内部结构

V8是一个很是复杂的项目,使用cloc统计可知,它居然有超过100万行C++代码

V8由许多子模块构成,其中这4个模块是最重要的:

  • Parser:负责将JavaScript源码转换为Abstract Syntax Tree (AST)
  • Ignition:interpreter,即解释器,负责将AST转换为Bytecode,解释执行Bytecode;同时收集TurboFan优化编译所需的信息,好比函数参数的类型;
  • TurboFan:compiler,即编译器,利用Ignitio所收集的类型信息,将Bytecode转换为优化的汇编代码;
  • Orinoco:garbage collector,垃圾回收模块,负责将程序再也不须要的内存空间回收;

其中,Parser,Ignition以及TurboFan能够将JS源码编译为汇编代码,其流程图以下:

简单地说,Parser将JS源码转换为AST,而后Ignition将AST转换为Bytecode,最后TurboFan将Bytecode转换为通过优化的Machine Code(其实是汇编代码)。

  • 若是函数没有被调用,则V8不会去编译它。
  • 若是函数只被调用1次,则Ignition将其编译Bytecode就直接解释执行了。TurboFan不会进行优化编译,由于它须要Ignition收集函数执行时的类型信息。这就要求函数至少须要执行1次,TurboFan才有可能进行优化编译。
  • 若是函数被调用屡次,则它有可能会被识别为热点函数,且Ignition收集的类型信息证实能够进行优化编译的话,这时TurboFan则会将Bytecode编译为Optimized Machine Code,以提升代码的执行性能。

图片中的红线是逆向的,这的确有点奇怪,Optimized Machine Code会被还原为Bytecode,这个过程叫作Deoptimization。这是由于Ignition收集的信息多是错误的,好比add函数的参数以前是整数,后来又变成了字符串。生成的Optimized Machine Code已经假定add函数的参数是整数,那固然是错误的,因而须要进行Deoptimization。

function add(x, y) {
    return x + y;
}

add(1, 2);
add("1", "2");
复制代码

在运行C、C++以及Java等程序以前,须要进行编译,不能直接执行源码;但对于JavaScript来讲,咱们能够直接执行源码(好比:node server.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为JIT。所以,V8也属于JIT编译器。

Ignition:解释器

Node.js是基于V8引擎实现的,所以node命令提供了不少V8引擎的选项,使用node的--print-bytecode选项,能够打印出Ignition生成的Bytecode。

factorial.js以下,因为V8不会编译没有被调用的函数,所以须要在最后一行调用factorial函数。

function factorial(N) {
    if (N === 1) {
        return 1;
    } else {
        return N * factorial(N - 1);
    }
}

factorial(10); // V8不会编译没有被调用的函数,所以这一行不能省略
复制代码

使用node命令(node版本为12.6.0)的--print-bytecode选项,打印出Ignition生成的Bytecode:

node --print-bytecode factorial.js
复制代码

控制台输出的内容很是多,最后一部分是factorial函数的Bytecode:

[generated bytecode for function: factorial]
Parameter count 2
Register count 3
Frame size 24
   18 E> 0x3541c2da112e @    0 : a5                StackCheck
   28 S> 0x3541c2da112f @    1 : 0c 01             LdaSmi [1]
   34 E> 0x3541c2da1131 @    3 : 68 02 00          TestEqualStrict a0, [0]
         0x3541c2da1134 @    6 : 99 05             JumpIfFalse [5] (0x3541c2da1139 @ 11)
   51 S> 0x3541c2da1136 @    8 : 0c 01             LdaSmi [1]
   60 S> 0x3541c2da1138 @   10 : a9                Return
   82 S> 0x3541c2da1139 @   11 : 1b 04             LdaImmutableCurrentContextSlot [4]
         0x3541c2da113b @   13 : 26 fa             Star r1
         0x3541c2da113d @   15 : 25 02             Ldar a0
  105 E> 0x3541c2da113f @   17 : 41 01 02          SubSmi [1], [2]
         0x3541c2da1142 @   20 : 26 f9             Star r2
   93 E> 0x3541c2da1144 @   22 : 5d fa f9 03       CallUndefinedReceiver1 r1, r2, [3]
   91 E> 0x3541c2da1148 @   26 : 36 02 01          Mul a0, [1]
  110 S> 0x3541c2da114b @   29 : a9                Return
Constant pool (size = 0)
Handler Table (size = 0)
复制代码

生成的Bytecode其实挺简单的:

  • 使用LdaSmi命令将整数1保存到寄存器;
  • 使用TestEqualStrict命令比较参数a0与1的大小;
  • 若是a0与1相等,则JumpIfFalse命令不会跳转,继续执行下一行代码;
  • 若是a0与1不相等,则JumpIfFalse命令会跳转到内存地址0x3541c2da1139
  • ...

不难发现,Bytecode某种程度上就是汇编语言,只是它没有对应特定的CPU,或者说它对应的是虚拟的CPU。这样的话,生成Bytecode时简单不少,无需为不一样的CPU生产不一样的代码。要知道,V8支持9种不一样的CPU,引入一个中间层Bytecode,能够简化V8的编译流程,提升可扩展性。

若是咱们在不一样硬件上去生成Bytecode,会发现生成代码的指令是同样的:

图片来源:Ross McIlroy

TurboFan:编译器

使用node命令的--print-code以及--print-opt-code选项,打印出TurboFan生成的汇编代码:

node --print-code --print-opt-code factorial.js
复制代码

我是在Mac上运行的,结果以下图所示:

比起Bytecode,正真的汇编代码可读性差不少。并且,机器的CPU类型不同的话,生成的汇编代码也不同。

这些汇编代码就不用去管它了,由于最重要的是理解TurboFan是如何优化所生成的汇编代码的。咱们能够经过add函数来梳理整个优化过程。

function add(x, y) {
    return x + y;
}

add(1, 2);
add(3, 4);
add(5, 6);
add("7", "8");
复制代码

因为JS的变量是没有类型的,因此add函数的参数能够是任意类型:Number、String、Boolean等,这就意味着add函数多是数字相加(V8还会区分整数和浮点数),多是字符串拼接,也多是其余更复杂的操做。若是直接编译的话,生成的代码好比会有不少if...else分支,伪代码以下:

if (isInteger(x) && isInteger(y)) {
    // 整数相加
} else if (isFloat(x) && isFloat(y)) {
    // 浮点数相加
} else if (isString(x) && isString(y)) {
    // 字符串拼接
} else {
    // 各类其余状况
}
复制代码

我只写了4个分支,实际上的分支其实更多,好比当参数类型不一致时还得进行类型转换,你们不妨看看ECMASCript对加法是如何定义的:12.8.3The Addition Operator ( + )

若是直接按照伪代码去生成汇编代码,那生成的代码必然很是冗长,这样会占用不少内存空间。

Ignition在执行add(1, 2)时,已经知道add函数的两个参数都是整数,那么TurboFan在编译Bytecode时,就能够假定add函数的参数是整数,这样能够极大地简化生成的汇编代码,伪代码以下:

if (isInteger(x) && isInteger(y)) {
    // 整数相加
} else {
    // Deoptimization
}
复制代码

固然这样作也是有风险的,由于若是add函数参数不是整数,那么生成的汇编代码也无法执行,只能Deoptimize为Bytecode来执行。

也就是说,若是TurboFan对add函数进行编译优化的话,则add(3, 4)add(3, 4)能够执行优化的汇编代码,可是add("7", "8")只能Deoptimize为Bytecode来执行。

固然,TurboFan所作的也不仅是根据类型信息来简化代码执行流程,它还会进行其余优化,好比减小冗余代码等更复杂的事情。

由这个简单的例子可知,若是咱们的JS代码中变量的类型变来变去,是会给V8引擎增长很多麻烦的,为了提升性能,咱们能够尽可能不要去改变变量的类型。

对于性能要求比较高的项目,使用TypeScript也是不错的选择,理论上,若是严格遵照类型化的编程方式,也是能够提升性能的,类型化的代码有利于V8引擎优化编译的汇编代码,固然这一点还须要测试数据来证实。

Orinoco:垃圾回收

强大的垃圾回收功能是V8实现提升性能的关键之一,由于它能够在避免影响JS代码执行的状况下,同时回收内存空间,提升内存利用效率。

关于垃圾回收,我在JavaScript深刻浅出第3课:什么是垃圾回收算法?中有详细介绍,这里就再也不赘述了。

JS引擎的将来

V8引擎确实很强大,可是它也不是无所不能的,简单地分析均可以发现一些能够优化的点。

我有一个新的想法,还没想好名字,不妨称做Optimized TypeScript Engine:

  • 使用TypeScript编程,遵循严格的类型化编程规则,不要写成AnyScript了;
  • 构建的时候将TypeScript直接编译为Bytecode,而不是生成JS文件,这样运行的时候就省去了Parse以及生成Bytecode的过程;
  • 运行的时候,须要先将Bytecode编译为对应CPU的汇编代码;
  • 因为采用了类型化的编程方式,有利于编译器优化所生成的汇编代码,省去了不少额外的操做;

这个想法其实能够基于V8引擎来实现,技术上应该是可行的:

  • 将Parser以及Ignition拆分出来,用于构建阶段;
  • 删掉TurboFan处理JS动态特性的相关代码;

这样作,能够将JS引擎简化不少,一方面再也不须要parse以及生成bytecode,另外一方面编译器再也不须要由于JavaScript动态特性作不少额外的工做。所以能够减小CPU、内存以及电量的使用,优化性能,惟一的问题多是必须使用严格的TS语法进行编程。

为啥要这样作呢?由于对于IOT硬件来讲,CPU、内存、电量都是须要省着点用的,不是每个智能家电都须要装一个骁龙855,若是但愿把JS应用到IOT领域,必然须要从JS引擎角度去进行优化,只是去作上层的框架是没有用的。

其实,Facebook的Hermes差很少就是这么干的,只是它没有要求用TS编程。

这应该是JS引擎的将来,你们会看到愈来愈多这样的趋势。

关于JS,我打算花1年时间写一个系列的博客**《JavaScript深刻浅出》**,你们还有啥不太清楚的地方?不妨留言一下,我能够研究一下,而后再与你们分享一下。欢迎添加个人我的微信(KiwenLau),我是Fundebug的技术负责人,一个对JS又爱又恨的程序员。

参考

关于Fundebug

Fundebug专一于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对一、微脉、青团社等众多品牌企业。欢迎你们免费试用!

img

版权声明

转载时请注明做者 Fundebug以及本文地址: blog.fundebug.com/2019/07/16/…

相关文章
相关标签/搜索