V8 引擎如何执行JS,之前看过 Webkit 技术内幕,也只是走马观花。并没有深入理解,突然看到这篇文章,翻译之How does the Google V8 engine work?
Google V8
引擎是如何工作的?这是一个非常好的问题,这里有少许流出的官方文档来讲解,到底 V8
内部都做了什么。我会把我知道的东西分享给你(你需要自己猜,哪部分我给拿掉了),还有很多有用的地址去帮助你明白这些内容。
V8
有两个编译器:
- 一个非常简单并且非常快的编译器用于将
js
编译成简单但是很慢的机械码,叫做full-codegen
。 - 另一个是非常复杂的实时优化编译器,编译高性能的可执行代码,叫做
Crankshaft
。
V8
里面也使用了一些线程:
- 主线程,做你希望它做的事情:加载你的代码,编译它们,执行他们。
- 有一个独立的编译线程,当主线程执行的时候,它去优化代码。
- 一个
profiler
线程(不知道还有没有了,但是会有一个同样职责的线程存在),用于发现执行过程中哪个方法耗费了大量时间,这样Crankshaft
就可以优化这些代码。 - 一些用于
GC
处理的线程(译者注:这里是一些用于GC
的线程,不止一个线程用于垃圾回收)。
最开始执行你的代码的时候,V8
开始使用 full-codegen
,full-codegen
直接将 JS
代码解释成机械码,没有做任何转化。这可以让 V8
快速执行机械码。注意,V8
并不使用中间字节码,因此也就不再需要转译处理。
当你的代码被执行的时候,profiler
线程有足够的数据来找出哪些方法需要被优化。我不确定 V8
如何选择使用哪个线程做的优化,简单起见,我们就认为它用的主线程吧。
主线程通过停止正在执行的代码(也许就在需要优化的方法这里),开始使用 Crankshaft
进行优化。JS
代码首先会被编译成一种叫做 Hydrogen
的高级描述,它是控制流图的静态单赋值表示。大多数优化都是在这个级别完成的。
首先,对尽可能多的代码进行内联,这使得优化变得更有意义。然后进行类型转化。这个优化移除了打包和拆包的处理,可以认为是执行了很多指令。这意味着,如果你的代码在操作整数,并且没有做类型转换,比如转换成 string
,double
这些,那么它会跑的很快。内联缓存会在这个阶段起到非常重要的作用,提供了类型判断。就像你猜到的那样,我们需要小心类型转换:如果你希望一个变量是一个整数,但是过一会却被修改成了其它类型,那么你的假设就失败了,那么一次重新编译就在所难免了。还有其它的优化,比如 loop-invariant code motion
(译者注:貌似是讲将循环内不变化的代码移到外面,减少每一次循环执行的代码数量),移除死代码(译者注:不被执行的代码也要移除,否则 V8
始终都要要对这些代码处理的,带来了额外负担)等。
一旦 Hydrogen graph
被优化,Crankshaft
会降低它到一个低级别的描述,叫做 Lithium
。大部分的 Lithium
执行于特定架构。分配寄存器就是在这个级别进行的。
最终,Lithium
被编译成机械码。然后一些叫做 OSR
(on-stack replacement
)的事情就发生了。记住,在我们开始编译和优化运行耗时较长的方法之前,我们喜欢先执行它。我们不要忘记我们刚刚放慢了执行,然后开始执行优化后的代码。相反,我们将要转换所有的上下文,因此我们才能在执行的中间过程中选择执行优化后的代码。我让你们感到复杂,提醒一下,其它的优化中,我们内联了一些东西。V8
并不是唯一一个这么做的虚拟机,但是我发现一些比较疯狂的地方。有一些保护机制叫 -- 去优化,在做相反的事情,并且会在一些假设的特定情况下反转一些优化后的代码。
还有。就是编译/执行部分。我忘了提到 GC
,不过这很短,因为我对它不太了解。
对于垃圾收集来说,V8
使用了传统的方法,采用标记计数的方式来进行垃圾收集。标记阶段必须停止 JavaScript
执行。为了控制 GC
成本,使执行更加稳定,V8
采用增量标记。这就是说,他们不是在堆中试图标记每一个可能的对象,而是处理一部分堆,然后恢复正常的执行。下一个 GC
停止执行代码的时候处理之前未处理的堆。这允许非常短的暂停。如前所述,扫描阶段由单独的线程来处理。
floitsch.blogspot.de/2012/04/opt…
还有这里源码