Google V8 引擎工作原理(翻译)

9,819 阅读5分钟

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-codegenfull-codegen 直接将 JS 代码解释成机械码,没有做任何转化。这可以让 V8 快速执行机械码。注意,V8 并不使用中间字节码,因此也就不再需要转译处理。

当你的代码被执行的时候,profiler 线程有足够的数据来找出哪些方法需要被优化。我不确定 V8 如何选择使用哪个线程做的优化,简单起见,我们就认为它用的主线程吧。

主线程通过停止正在执行的代码(也许就在需要优化的方法这里),开始使用 Crankshaft 进行优化。JS 代码首先会被编译成一种叫做 Hydrogen 的高级描述,它是控制流图的静态单赋值表示。大多数优化都是在这个级别完成的。

首先,对尽可能多的代码进行内联,这使得优化变得更有意义。然后进行类型转化。这个优化移除了打包和拆包的处理,可以认为是执行了很多指令。这意味着,如果你的代码在操作整数,并且没有做类型转换,比如转换成 stringdouble 这些,那么它会跑的很快。内联缓存会在这个阶段起到非常重要的作用,提供了类型判断。就像你猜到的那样,我们需要小心类型转换:如果你希望一个变量是一个整数,但是过一会却被修改成了其它类型,那么你的假设就失败了,那么一次重新编译就在所难免了。还有其它的优化,比如 loop-invariant code motion(译者注:貌似是讲将循环内不变化的代码移到外面,减少每一次循环执行的代码数量),移除死代码(译者注:不被执行的代码也要移除,否则 V8 始终都要要对这些代码处理的,带来了额外负担)等。

一旦 Hydrogen graph 被优化,Crankshaft 会降低它到一个低级别的描述,叫做 Lithium。大部分的 Lithium 执行于特定架构。分配寄存器就是在这个级别进行的。

最终,Lithium 被编译成机械码。然后一些叫做 OSR (on-stack replacement)的事情就发生了。记住,在我们开始编译和优化运行耗时较长的方法之前,我们喜欢先执行它。我们不要忘记我们刚刚放慢了执行,然后开始执行优化后的代码。相反,我们将要转换所有的上下文,因此我们才能在执行的中间过程中选择执行优化后的代码。我让你们感到复杂,提醒一下,其它的优化中,我们内联了一些东西。V8 并不是唯一一个这么做的虚拟机,但是我发现一些比较疯狂的地方。有一些保护机制叫 -- 去优化,在做相反的事情,并且会在一些假设的特定情况下反转一些优化后的代码。

还有。就是编译/执行部分。我忘了提到 GC,不过这很短,因为我对它不太了解。

对于垃圾收集来说,V8 使用了传统的方法,采用标记计数的方式来进行垃圾收集。标记阶段必须停止 JavaScript 执行。为了控制 GC 成本,使执行更加稳定,V8 采用增量标记。这就是说,他们不是在堆中试图标记每一个可能的对象,而是处理一部分堆,然后恢复正常的执行。下一个 GC 停止执行代码的时候处理之前未处理的堆。这允许非常短的暂停。如前所述,扫描阶段由单独的线程来处理。

posts tagged "v8"

Posts Tagged 'v8'

V8 Resources

thlorenz/v8-perf

docs.google.com/document...

floitsch.blogspot.de/2012/04/opt…

还有这里源码