All Articles

V8 Blog | Background compilation 2018-03-27

1. 原文

Background compilation

2. 摘要翻译

从Chrome 66开始,V8会在后台线程中进行JS源码的编译,减少主线程5% - 20%在典型网站上的编译时间。

Background

自从版本41开始,Chrome已经支持了通过V8的StreamedSource API在后台线程上编译JS源码文件。这使得V8能够在Chrome一下载完文件的第一个chunk之后就立即开始解析源码文件,并且一边从网络下载文件一边持续并行解析文件的后续内容。这提供了相当大的加载效率提升,因为V8几乎能够一下载完成JS源码文件就将其解析完成。

然而,因为V8原来的基准编译器的限制,V8仍旧需要回到主线程来完成解析和编译脚本成JIT机器码(最终运行的脚本代码)。在将其切换成我们最新的Ignition + TurboFan管线之后,我们现在可以将字节码编译工作也交给后台线程处理,从而将Chrome的主线程解放出来专注在向用户交付一个更流畅响应更快的浏览器体验。

Building a background thread bytecode compiler

V8的Ignition字节码编译器(bytecode compiler)将解析器(parser)制造的抽象语法树(abstract syntax tree (AST))作为输入,伴随着相关的meta-data一起流式产出对应的字节码(BytecodeArray)输出,使得Ignition解释器能够执行JS源码。

Ignition的字节码编译器从设计之初就被设计为多线程构造,但是要实现后台编译仍旧需要引入一些针对编译管线的改动。最主要的改动之一是防止编译管线在后台线程上运行的时候访问V8堆内的对象。V8堆上的对象并非线程安全(thread-safe)的,因为JS总是单线程的(single-threaded),并且这些对象可能在后台编译期间被主线程或V8的垃圾回收器改动。

编译管线有两种场景需要访问V8堆内对象:抽象语法树内化(AST internalization),和字节码终结(bytecode finalization)。抽象语法树内化(AST internalization)是一个流程。在这个流程从AST内定位出来的字面量对象(literal objects:strings、numbers、object-literal boilerplate等)会被分配到V8堆内,这使得它们后续能够在脚本执行的时候被生成出来的字节码直接使用。这个流程按惯例是在解析器(parser)制作完AST之后直接发生的。同样的,编译管线后续的几个步骤也会依赖这些已经被分配的字面量对象。为了后台编译,我们将AST的内化放到了编译管线的后面,字节码编译完成之后。这就要求编译管线对后续的几个场景进行改动,使用内嵌在AST内的裸的字面量值来替代初始化在V8堆上的值。

字节码终结(bytecode finalization)包含了构建用来执行函数的最终字节码流对象(BytecodeArray object),以及于此相关的metadata - 举例来说,被字节码引用用来存储常量的ConstantPoolArray,和用来映射源码中行列数值到字节码的偏移量的SourcePositionTable。因为JS是一个动态语言,这些变量需要存活在JS的堆内存中,以便后续与字节码对应的JS函数被回收之后他们也能被回收掉。之前一部分metadata对象会在字节码编译期间被分配并且被修改,这会导致对堆内存的访问。为了实现后台编译,Ignition的字节码生成器被重构来跟踪这些metadata的细节(details),并且推迟它们在JS堆上的内存分配直到编译的最后流程。

在引入了这些改动之后,几乎所有的源码编译工作能被移动到后台线程中执行,仅只有相当快速的AST内化和字节码终结步骤发生在主线程执行代码脚本之前。

目前,仅只有顶层的脚本代码(top-level script code)以及立即执行的函数表达式(immediately invoked function expressions (IIFEs))是在后台线程进行编译的 - 内部函数(inner functions)仍旧是在主线程上惰式编译的(当第一次运行的时候)。我们希望后续能将后台编译推广到更多场景上。不管怎么说,即便拥有目前的这些限制,后台编译仍旧可以使主线程得到长时间的解放,让主线程能够做其他工作,例如与用户交互、渲染动画或是其他任何能够创造一个更流畅和响应更快的体验的工作。

Results

我们使用我们的模拟真实(real-world)benchmark框架访问一系列主流的站点,来评估后台编译的性能提升。

能够在后台线程发生的编译占比 vs 在顶层流式脚本(top-level streaming-script)编译中得到编译的字节码占比 vs 作为内部函数(inner functions)调用而被惰式编译的占比(这部分仍旧必须在主线程编译)。同样的,主线程得到节省的时间占比 vs 大部分的页面可见5% - 20%的主线程编译时间节约。
(这段翻译感觉味道有点不对,我放出原文,希望大家能留言指正)

The proportion of compilation that can happen on a background thread varies depending on the proportion of bytecode compiled during top-level streaming-script compilation verses being lazy compiled as inner functions are invoked (which must still occur on the main thread). As such, the proportion of time saved on the main thread varies, with most pages seeing between 5% to 20% reduction in main-thread compilation time.

Next steps

有什么方法比在后台线程编译代码脚本更好?那就是根本不用编译脚本文件!除了后台编译之外,我们还在尝试提升V8的代码缓存系统来扩大能被V8缓存的代码量,来加速你经常访问的网站的速度。我们希望能很快带来更新的消息。

EOF

Published 2018/3/27

Some tech & personal blog posts