走向核心的探索 — V8引擎初探

1,511 阅读5分钟

前言

这段时间在写编译原理的课设,对于编译器的实现算是入了个门,着就激起了我心中的一个本源问题,JavaScript的引擎到底是什么样子的,V8直接导致了Node.js时代,JavaScript能做的事情越来越多。作为一个出色的JavaScript引擎,他的模式值得我们思考和学习。

那么V8引擎到底是怎么工作的呢?

两个编译器的故事

V8会编译所有JavaScript到原生代码,而在V8中,有两个编译器在运行着:一个运行比较快,输出着一般的代码,另一个运行的没有那么快,但是尽力的输出着优化过的代码。

第一个编译器 — full-codegen 编译器

输出一般代码的那个编译器在内部被称为:full-codegen(全代码生成) 编译器。它接受一个函数的抽象语法树,遍历语法树,直接生产汇编代码。通过获取被解析过的函数源代码(抽象语法树),带有类型记录缓存的原生代码。它是一个很一般的编译器,运行了一般编译器从语法分析后直到代码生成的过程。

所有本地的变量没有被存放在寄存器中,而是都放在了栈或者堆里面。所有被嵌套函数引用的变量全部被存在了堆里面,这个堆决定了在函数上下文中,哪些函数被定义了。编译器会根据情况把这些值放进寄存器里面,并执行具体工作。而对于存在栈里面的变量,栈顶的几个变量会暂时的在寄存器内缓存。而更复杂的情况,则有实时处理程序来管理。这个编译器会记录语句执行的上下文,这样就能直接跳到需要执行的块,而不是把变量放进寄存器,测试这个量是不是0,然后产生分支(大概就是汇编里面的TEST,JNZ的步骤吧)。类似于简单的算数求值也会在这里被优化进行。

这个编译器使用了一个非常重要的技术来优化代码—— inline caching。编译的时候有这样的缓存,直接可以用于赋值、一元运算、二元运算、函数调用、属性获取还有比较值。inline caching 还向另一个优化编译器提供了类型源数据。而inline caching 在编译的时候,缓存了键和值的储存,而一般的操作并不会触发inline caching。

类型反馈

当V8引擎第一次看到一个函数的时候,他只直接建立语法树,不做其他事情。直到第一次调用函数的时候,V8才第一次跑full-codegen 编译。但这种有点偷懒的做法,在代码开始运行后有了变化。运行开始后,会触发分析线程,这个线程负责看看代码跑的怎么样,那些函数是热点函数。

这种偷懒,静观其变的做法,让V8引擎可以跟踪类型变化,记录相应数据。当V8发现了热点函数,觉得这个函数可以帮一把的时候,他就把类型反馈数据给编译器。运行时的类型反馈数据会被记录。

         Unknown
           |   \____________
           |                |
      Primitive       Non-primitive
           |   \_______     |
           |           |    |
        Number       String |
         /   \         |    |
    Double  Integer32  |   /
        |      |      /   /
        |     Smi    /   /
        |      |    / __/
        Uninitialized.

每次看到新的值,就计算这个值的类型,然后和旧的值类型进行运算。最初的变量类型是Uninitialized(未初始化)。所以当看到一个整型的时候,如果他的大小在Smi (small integer) 范围内的时候,会直接在类型反馈里面推断,它是一个Smi。但是当看到这个值变成了Double了,那么做运算后,这个值的推断直接变为Number。推断每次的结果就是寻找了两个值的最近共同parent。在内部做了类型预估,让编译器可以有目的的优化。

类型反馈数据和抽象语法树是相互联系的,函数的热度是由一个整型记录的,相应的从full-codegen获取热点节点标记信息,并把这些信息送给编译器做进一步优化。

到了这里,这个过程开始变得有一些复杂了。这个过程里面需要实现对于编译器栈的向上向下兼容。编译器需要获得操作数和结果的类型反馈,并且还要能准确的找到这个数据。然后你还要能够把这些东西重新和抽象语法树关联起来,编译器才能从语法树有目的优化代码。

V8在这个过程上,通过把数据分析成TypeFeedbackOracle对象,并且把这个对象和特定的语法树节点联系。最终,V8通过访问语法树的节点就会通过这个对象进行,这个对象也能够优化编译过程。

第二个编译器 — crankshaft 编译器

一旦V8确定了热点函数,得到了类型反馈的信息,他就会尝试带着这些信息来运行优化编译器。优化编译器在市面上称为crankshaft(轴心) 编译器,虽然在源代码上面并没有这样命名。实际上,crankshaft 编译器在源代码里面是由四个过程组成的:带有类型反馈的抽象语法树->高级别中间代码->低级别中间代码->优化过的原生代码。

高级别中间代码是编译器前端形成的代码,而低级别中间代码是后端使用的中间代码,通过前端后端双重的优化,让V8引擎对热点函数有更好的处理。

总结

V8引擎采用惰性优化的方式来提高性能,通过针对运行时的热点函数优化,快编译和慢优化的结合,而且通过合理的类型推断解决的JavaScript的类型问题。这只是对V8早期版本的一个概念分析,但是我已经开始接触到了V8优化的魔法。

参考资料:

http://wingolog.org/archives/2011/07/05/v8-a-tale-of-two-compilers#ffc2b5d74c27fa60d75658244fee88e6fa783afb

https://github.com/v8/v8/tree/master/src