传送门:
- Introducing the B3 JIT Compiler <- 这篇介绍写得非常好,清晰的讲解了B3的设计目的、思路和效果。
- B3文档:Bare Bones Backend
- B3 IR文档:B3 Intermediate Representation
Filip大大再次立功。苹果内部果然也开撕了嘛…
在这次更新后,JavaScriptCore仍旧会使用4层编译系统:
- 第1层:解释器 LLInt
- 第2层:简易非优化JIT编译器 Baseline JIT
- 第3层:优化JIT编译器 DFG JIT
- 第4层:高度优化JIT编译器 FTL JIT
只不过,这个FTL的意思变了:
- 老:Fourth Tier LLVM = DFG++(高层优化) + LLVM(底层优化)
- 新:Faster Than Light = DFG++(高层优化) + B3(底层优化)
…名字的更新当然是故意的。以前就有人开玩笑说FTL是faster than light,现在把LLVM扔掉了这名字就干脆也改过来了。
可以看到其实这个新的B3后端并不是把整个FTL JIT给重写了,而只是替换掉了其中负责底层优化的LLVM部分而已。
不得不说Filip大大带的JavaScriptCore团队还挺厉害的。去年10月开始写这个新的名为“B3”的后端,过了4个月就已经上了正轨了。
B3本质上是对LLVM的剪裁与特化,针对JavaScriptCore的需求而生。它与LLVM在相似的抽象层,B3 IR的构造API也与LLVM IR相似,这样FTL::Output就可以方便的从原本构造LLVM IR切换到构造B3 IR。换句话说,扔掉的是LLVM“臃肿”的实现,而保留下的是LLVM的优化的精髓。当然“臃肿”是相对FTL的应用场景而言的(?)。
这个B3后端最最核心的关注点就是减少内存开销。这再次展示了编译器中IR在内存里的布局对编译速度的影响——但主要关注AOT编译场景的编译器(例如GCC和LLVM)大多不够注重这一点。在FTL JIT里,LLVM编译出来的代码质量虽然不错,但是编译速度在许多运行时间不够长的场景里就显得太慢了,用户程序都跑完了LLVM还没编译完。
以前跟JRockit的JIT编译器开发聊天的时候,他们也提到JRockit的JIT编译器IR有过一次重要的更新,大幅减少了内存开销而并没有做什么别的特别优化,光是这样就让编译速度提升了50%而编译出来的代码质量与以前一样。JRockit减少编译器IR的内存开销主要是通过几种思路:
- 减少数据结构中指针的使用,改为用更紧凑的整数ID/下标来表示引用关系;
- 尽量用固定大小的结构表示常用信息,而把可变长并且不常用的信息挪到外部;
- 尽量把数据紧凑的放在数组/vector里。
无独有偶,B3的思路和JRockit非常非常相似。这也是比较新的、干净的IR设计流行的做法。
B3为了减少内存开销,还把LLVM IR中的use-def/def-use双向链接中的def-use信息抛弃了。一个def无法直接找到自己的所有use,就无法直接完成“Replace All Uses With”操作;但要替换IR指令还是可以通过遍历一次IR图来做到。
不在IR里记录def-use信息是的有趣的取舍;HotSpot Client Compiler(C1)与Maxine C1X也采用了类似的设计,只为了SSA而记录use-def信息,但不记录def-use信息。这个设计在C1里却造成了一些很悲催的坑:C1 HIR的指令替换只能一条对一条的做——一条HIR指令可以被另外一条HIR指令所替代,但是一条新的指令却无法替代多条老的指令,因为不遍历一次整个IR就无法知道老指令有没有被其它地方用到(同时C1也无法让多条新指令替代一条老指令,不过那是另一个问题)。这设计导致C1某些优化被“永久禁用”,导致C1X的DiamondEliminator如此难看、而SCCPropagator实现起来太麻烦以致没人去实现它。
但C1 / C1X的HIR还是用单向链表连在一起的,而B3 IR是用数组(vector)为容器,如果要实现批量插入新IR节点那肯定得仔细处理才能让效率高。B3的解决方案是在一个pass里把需要对IR做的修改放进InsertionSet里,等到下次遍历整个IR图时再去批量插入IR。这对JavaScriptCore倒不是什么新概念——DFG自己其实早就有InsertionSet了。可见Filip大大还是不禁觉得自己的设计比LLVM好哇。
B3 IR里还藏了些原本LLVM没有但DFG已经有的私货,例如UpsilonValue。这是DFG所采用的特殊的SSA形式的组成部分,既然要扔掉LLVM,就干脆把B3的SSA形式也向DFG靠拢。
B3 IR里一些复合控制流的IR指令设计也是个有趣的点。用Graal的术语来说,这就是在IR层面上用更显式的方式来表达Guard。在IR层面上保留Guard的语义,而不急于将其lower到普通的if + deoptimization,对干净的实现某些优化(例如guard的提升与合并)还挺重要的。
B3目前采用的寄存器分配器是一个graph coloring算法的变种,叫做Iterated Register Coalescing(IRC)。LLVM默认的寄存器分配器则是一种linear scan算法的比较复杂的变种,叫做Greedy。Greedy虽然比原始的linear scan慢一些,但是总体来说还是比graph coloring系的算法快而效果相近。所以Filip大大也说会一边继续优化B3基于IRC的寄存器分配器,一边考虑把Greedy分配算法移植到JavaScriptCore里。
LLVM的Greedy寄存器分配器近来还挺流行的。V8 TurboFan尝试过移植它(虽然目前被撤销了,但以后或许还会再放进来),IonMonkey也实现了它。Hmm。
====================================================
同事Philip Reames也发表了对B3的看法:Quick thoughts on WebKit’s B3
显然,作为我们这边的重度LLVM用户和开发,Philip大大对B3颇有微词 >_<
====================================================
题图跟本文的主题有什么关系,大家知道不?^_^
(题图引用自戦闘妖精雪風,(c)2002 神林長平・早川書房/バンダイビジュアル・ビクターエンタテインメント・GONZO)
专栏:编程语言与高级语言虚拟机杂谈(仮)
探讨编程语言的设计与实现