v8: a tale of two compilers



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

this article and all articles in this site were mostly translated by Google translate with a little human polishing.

普通读者会注意到我对V8 JavaScript实现的迷恋。这确实是令人印象深刻的工程。

当V8最初宣布时,Lars Bak写道:

我希望网络社区将采用我们开发的代码和想法来提高JavaScript的性能。提高JavaScript的性能标准对于Web应用程序的持续创新非常重要。

不仅采用V8是成功的,而且在所有JavaScript实现中的“提高性能”中取得了令人瞩目的成就。

但正如威廉·吉布森所说:“未来已经在这里 - 只是分布不均匀。” 考虑到事情发生的变化,V8的许多部分根本没有记录,也许可以理解。所以当我正在加快V8与Igalia的合作时,我一直在努力记录我发现的有趣的事情,所以所有的JavaScript实现都可以学习和改进。

事实上,V8的这项研究给了我很多的想法和动机。所以也许V8的新座右铭应该是“把世界的代码变的更快,只需一个编译器”。

第一个编译:full-codegen

V8将所有JavaScript编译为本地代码。 V8有两个编译器:一个运行速度快,并且生成通用代码,而不是运行速度不高但尝试生成优化代码的编译器。

快速简单的编译器在内部被称为“全代码”编译器。 它作为函数的抽象语法树(AST)作为其输入,遍历AST中的节点,并直接发出对宏程序集的调用。 这是一张照片:

http://wingolog.org/pub/v8-full-codegen.svg

这些框表示编译过程中的数据流。只有两个框,因为正如我们所说,这是一个简单的编译器。所有局部变量都存储在堆栈或堆上,而不是存储在寄存器中。嵌套函数引用的任何变量都存储在与定义变量的函数关联的上下文对象中的堆上。

编译器开始加载和存储,以将这些值拉入寄存器以实际执行此工作。临时堆栈的顶部被缓存在一个寄存器中。复杂的情况通过调用运行时程序来处理。编译器会跟踪正在评估表达式的上下文,以便测试可以直接跳转到后续块,而不是将一个值push进缓存,测试是否为零,然后再进行分支。小整数算术通常是内联的。

实际上,我应该提到即使使用全代码编译器也是一个重要的优化,那就是内联缓存。请参阅Hölzle,Chambers和Ungar的论文【http://wingolog.org/archives/2008/10/19/dynamic-dispatch-a-followup】。内联高速缓存用于分配,一元和二进制操作,函数调用,属性访问和比较。

内置缓存也可用作优化编译器使用的类型信息的来源。在某些语句类型(如赋值)的情况下,IC的唯一目的是记录类型信息.

ast.h
The abstract syntax tree.

full-codegen.h
full-codegen.cc
full-codegen-ia32.cc
全代码编译器。 全代码编译器的大多数关键内容都在目标特定目录(4257行vs 769 + 1323行)。 目前支持的架构是ia32,x64,arm和mips。

类型反馈

V8第一次看到一个函数,它会把函数解析为AST,但实际上并没有做任何事情。 当函数首次运行时,它只运行全代码编译器。 懒惰怎么样 但是,事情开始之后,它启动了一个剖析线程,看看事情发生了,什么功能很热。

这种懒惰的坐在后视观看方式使V8能够记录流经它的类型信息。 所以在决定一个函数是否会被经常访问的时候,可以使用类型来获得一点帮助,它有一个传递给编译器的类型信息。

运行时类型反馈信息被记录并存储在内联高速缓存(IC)中。 类型反馈信息在内部表示为以这样的方式构造的8位值,使得它可以用简单的位掩码来检测类型的层次。 在这一点上,我能做的最好的就是通过源代码展示艺术品:

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

每当一个IC存根看到一种新的值时,它会计算该值的类型,并按比例将其与旧类型相对应。初始化类型值未初始化。所以如果IC只能看到Smi(小整数)范围内的整数,记录的类型将会指示。但是一旦它看到一个double值,那个类型就变成了数字;如果它看到一个对象,那么该类型将变为“未知”。非原始IC必须将接收器类型的映射存储在IC中,以便传递。在需要时,类型反馈可以解析IC stub以获取此map。

类型反馈信息与特定的AST节点(分配,属性负载等)相关联。节点的整数标识符被序列化到IC中,因此当V8决定函数经常被调用时,它可以从全代码代码解析记录的类型信息,并将其与AST节点相关联。

这个过程有点复杂。它需要在编译器堆栈中上下支持。你需要有内联缓存。您的内联高速缓存需要支持类型信息,包括操作数和结果。您需要能够遍历这些数据才能找到值。然后,您需要将其链接回AST,以便在将AST传递给优化编译器时,编译器能够提出正确的问题。

V8采取的具体策略是将数据解析为TypeFeedbackOracle对象,将信息与特定的AST节点相关联。然后V8使用这个oracle访问所有的AST节点,节点本身解析出他们可能会从oracle发现有用的数据。

最后,例如,可以询问Property节点是否是单形,在任何情况下,该节点的接收器类型是什么。看来这对于V8来说很好,因为它减少了优化编译器中的移动部件的数量,因为它不需要具有TypeFeedbackOracle本身。

type-info.h
TypeInfo 8位数据类型和TypeFeedbackOracle声明。 我不得不承认,我真的很喜欢在V8中使用C ++。 这是一个令人讨厌的工具,但他们很好。

type-info.cc
TypeFeedbackOracle的实现。 请参阅文件底部的ProcessTarget。
还要检查ast.h链接,看看类型反馈如何与AST本身联系在一起。

曲轴=类型反馈+氢+锂

一旦V8确定函数经常被调用,并收集了一些类型的反馈信息,它会尝试通过优化编译器运行增强的AST。 这种优化编译器被称为Crankshaft ,尽管该名称很少出现在源代码里。

相反,Crankshaft 由Hydrogen 高级中间表示(IR),Lithium 低级别IR及其相关的编译器组成。

Like this:
http://wingolog.org/pub/v8-crankshaft.svg

(我相信“氢(Hydrogen)”和“锂(Lithium)”的名称分别来自高(High-)低(Low-)层。)

取决于你的背景知识,但你可能已经看到过这样的图:

http://www.stanford.edu/class/cs343/resources/java-hotspot.pdf

事实上,我相信Crankshaft受到Sun在Java 6中引入热点客户端编译器的更改的高度影响。让我引用Kotzmann等人的“2008年热点客户端编译器设计”的一段话

首先,通过对字节码的抽象解释来构建编译方法的高级中间表示(HIR)。它由一个控制流图(CFG)组成,其基本块是指令的单链表。 HIR是静态单一赋值(SSA)形式,这意味着对于每个变量,程序中只有一个点被赋值给它。加载或计算值的指令表示操作及其结果,因此操作数可以表示为指向先前指令的指针。在HIR生成期间和之后,执行若干优化,例如恒定折叠,数值编号,方法内联和空检查消除。他们受益于HIR和SSA形式的简单结构。

编译器的后端将优化的HIR转换为低级中间表示(LIR)。 LIR在概念上类似于机器代码,但仍然与平台无关。与HIR指令相反,LIR操作操作在虚拟寄存器上,而不是对先前指令的引用。 LIR有助于各种低级优化,也是线性扫描寄存器分配器的输入,它将虚拟寄存器映射到物理寄存器。

该声明非常整齐地描述了Crankshaft,该论文的第2部分的其余部分在一般意义上适用。当然有一些区别。Crankshaft以AST开头,而不是字节代码。 HotSpot客户端运行时不使用类型反馈来帮助其编译器,因为它对Java不太必要,尽管它仍然有帮助。Crankshaft对异常处理程序不会做很多工作。

但是相似之处在于,V8实际上可以产生由c1visualizer(docs)读取的跟踪,这是一个用于可视化HotSpot客户机编译器内部的程序。 (客户端编译器似乎在内部被称为c1;服务器编译器似乎是opto的)。

Thursday, August 24, 2017 by blast