JIT编译器杂谈#1:JIT编译器的血缘(一)

JIT编译器杂谈#1:JIT编译器的血缘(一)

这年头啥都得讲个娱乐性。专栏第一篇杂谈,先来点八卦轻松一下。

对我来说,有没有人最近用DJI无人机求婚成功啥的如同耳边一阵风;上周CoreCLR在GitHub上以MIT许可证开源了才是激动人心的娱乐新闻啊!

趁着这个娱乐热点,从CLR的JIT编译器引伸出去,我想在这篇杂谈写一些JIT编译器的血缘。正好可以从一个侧面解答:有那么多讲编译原理的书,为什么没有专门讲JIT编译器的?——因为JIT编译器用的也是“编译原理”啊(好吧还是有许多JIT的专有知识的,没多少专门的书确实可惜)。



从现成的编译器到JIT编译器


如果有个项目急需为某个语言实现一个优化的JIT编译器,怎样能在有限的时间内快速做出优化程度足够好的实现呢?

一个思路:如果有现成的静态编译器后端的话,针对输入的语言写个编译器前端,让它生成现成的后端能接受的IR,直接插到现成的后端上。

“有现成的静态编译器后端”门槛挺高,直到LLVM普及之前;不过土豪大厂们早已跨过这门槛,自然会想走这条路。


Microsoft CLR / JIT64

微软的桌面/服务器版CLR在RyuJIT之前有若干JIT编译器:

  • JIT32(mscorjit.dll):CLR一直以来的“Standard JIT”,或者叫“Normal JIT”,或者就叫“JIT”。最初主要针对32位客户端机器,所以这个编译器就定位在“Client Compiler”上,目标是快速编译,做少量开销低收益高的优化。运行32位CLR时用的是这个编译器(包括WOW64的场景)。
  • JIT64(mscorpjt.dll):今天的主角。从.NET Framework 2.0开始引入到64位版CLR。主要用于支持x64和Itanium(IA-64)平台。当初认为这样的64位平台肯定都是“服务器”,所以这个编译器定位在“Server Compiler”上,目标是尽可能编译出优化的代码,而编译速度是次要目标。运行64位CLR时用的时这个编译器。
  • EconoJIT(mscorejt.dll):只存在于.NET Framework 1.0时代的CLR。这是个编译速度非常快的编译器,完全不做优化,甚至连代码校验(verification)都不做,尽可能快速把MSIL转换为机器码。它还支持“抛弃代码”(code pitching)——把当前没有正在被调用的方法的JIT编译代码从code cache删除掉并回收相应的空间(“正在被调用”指的是当前在调用栈上有栈帧的方法)。这个功能在桌面CLR的其它JIT编译器都没有支持;在.NET Compact Framework的CLR里倒是有支持。在早期.NET Framework SDK里有一个JIT Compiler Manager(jitman.exe)可以配置CLR是用Standard JIT还是EconoJIT。
  • OptJIT(mscorojt.dll):与OptIL搭配使用,实现快速且高质量的JIT编译。似乎从来没正式在产品里发布?(有误请回复指正,谢谢!手上没老Windows机器不方便验证)OptIL是MSIL的一个子集,外加额外的元数据来引导JIT编译器做优化。应用场景是:先在静态编译的时候做大量耗时的优化,并把优化结果以元数据的形式嵌入OptIL里;JIT编译时可以借助元数据提供的“优化提示”来快速生成高质量的代码。
  • FJIT(mscorejit.dll):严格说不是桌面/服务器版CLR的JIT编译器,而是Shared Source Common Language Infrastructure (SSCLI) "Rotor"带的JIT编译器。不过SSCLI 2.0带的FJIT完全可以插入桌面版CLRv2使用,所以这里也算上它。从外部无法得到CLR的源码所以我无法确定,不过看起来FJIT其实就是以前的EconoJIT,外加少量更新(例如添加verification功能);连DLL文件的名字都一样,而且也支持“抛弃代码”。不然光为了向学术研究社区开放代码专门新写一个JIT编译器也…好吧其实也花不了多少功夫,这个编译器实在太简单了。FJIT没有自己的IR,每条MSIL指令对应一小块汇编模版,一趟遍历就直接从MSIL翻译到机器码。
  • MSILC JIT:嗯?这个是啥来的?等CoreCLR放出它的代码之后再看看。Add info about MSILC JIT to docs · Issue #253 · dotnet/coreclr · GitHub

可以看到JIT64是上述几个编译器里唯一一个以生成高质量代码为主要目标,而同时又可以不那么在乎编译速度的;而微软已经有一个出名的静态编译器了:Visual C++!正好符合这段的主题:把静态编译器的后端安到JIT编译器里。

JIT64基于UTC(Universal Tuple Compiler)。UTC同时也是Visual C++编译器的后端。VC++有明确的前端(c1xx.dll)、后端(c2.dll)和链接器(link.exe)的边界。前后端之间传递数据的格式是“CIL”(或CxxIL),是“C Intermediate Language”(或C++ Intermediate Language);不要跟.NET的Common Intermediate Language弄混。请参考Optimizing C++ Code : Overview

JIT64并不直接使用VC++的c2.dll,而多半是引入了UTC的代码在自己的项目里单独维护。毕竟还是JIT编译,JIT64不能直接“暴力”的把UTC的所有优化都用上,而必须精心挑选一些效果好的优化按照一定的顺序执行。

JIT64自己要做的事情就是把输入的MSIL和类型信息转换为UTC所使用的线性IR,然后到代码生成的时候再帮忙生成一下调试符号信息和GC所需的元数据就好了,其它都交给现成的UTC去解决,消除冗余、循环优化、基于图着色的寄存器分配,生成x64、Itanium的代码生成器,应有尽有。

听起来很美好对不对?但因为不是直接用VC++组的c2.dll而是引入了UTC的源码,这块代码变成JIT64要自己维护的负担;而且一开始没有考虑要在JIT编译器里使用的编译器后端在架构和实现上通常不太在乎编译速度和内存开销,很难后天补救,要用只能忍。

而且据说JIT64在做Itanium支持的时候还是坑了很久…hmm。

随着64位电脑的普及,现在随便找个x86的笔记本都是64位的,甚至连手机也开始用64位了,把64位机器都看作“服务器”的观点显然过时了。JIT64越来越多被吐槽编译速度太慢,于是终于在.NET Framework 4.6里被RyuJIT所替代。它还没彻底消失,在.NET 4.6还以compatjit.dll的名字作为备用JIT编译器待机——配置useLegacyJit=1的话还能继续用它。配置方法在这里有提到(Visual Studio "14" CTP 4 (version 14.0.22129.1.DP) -> Known Issues -> CLR下面。这是VS2015的技术支持说明,但同样的配置在.NET 4.6上应该也可以用)。



Sun ExactVM / JBE

无独有偶,相近时期Sun开发的JVM之一——ExactVM(EVM)——也借助了Sun当时已有的静态编译器后端来实现优化的JIT编译器。这个我知道的稍微多一些,可以多写点;从这个例子可以反过来猜测JIT64研发时的历程。

(好吧ExactVM的JBE应该是在CLR的JIT64之前开发的。JBE大概是从1997年开始研发,并在Sun JDK 1.2.2时期(1999年7月)发布在Solaris版JDK产品中;JIT64随CLRv2发布,.NET Framework 2.0于2006年1月发布,1.1于2003年4月,1.0于2002年2月,即便JIT64是1999-2000年开始研发的那也还是在JBE之后。)

ExactVM是Sun的“正统”JVM继承者。它的代码源于Sun JDK 1.0/1.1时代的JVM(后来叫做“Classic VM”),由Sun Labs的Java Topics Group负责研发。这组人本来想研究如何提高JVM的GC性能,结果拿到Classic VM之后发现执行引擎自身实在太慢,GC的性能问题根本体现不出来!一帮人只好先去解决执行引擎的效率问题,所以就开始研发新的优化JIT编译器。

Classic VM在Sun JDK 1.1时代有一个用汇编写的解释器,效率还不错;还有一个性能和稳定性都一般的JIT编译器,“sunwjit“(Sun Workshop JIT)。ExactVM想要尽快得到一个高度优化的JIT编译器来填补高端部分的空缺,但是Labs哪儿来的人力物力去做这件事呢?他们就跟产品组合作,专门针对Solaris开发新的优化JIT编译器,并且找隔壁的Sun Workshop编译器组弄来他们的代码和开发参与进来。这就是JBE(Java Back End)。更加“根正苗红”了,全套Sun的自家装备。

在ExactVM里,JBE与解释器、sunwjit组成一个“多层编译系统”:

  • Java方法刚开始都由解释器执行;
  • 足够热之后会由充当初级编译器的sunwjit编译。这个是前台编译,也就是说触发编译的Java线程会暂停下来等编译;
  • 再继续执行足够热之后会再由JBE优化编译。这个是后台编译,在一个单独的编译器线程上运行,也就是说触发编译的Java线程在触发后可以继续执行,同时编译任务会在后台的编译器线程执行,什么时候编译好就什么时候开始用新编译的代码。

觉得眼熟不?没错,现在的HotSpot VM的多层编译系统大体上看也是这样设计的。不过当时ExactVM的实现还是没有现在HotSpot VM的实现干练,而且也没有实现OSR,跑小型性能测试程序会略吃亏。

ExactVM对这个系统的编译器非常有信心,觉得大部分时间都应该在执行JIT编译后的代码,所以解释器性能就不那么重要了。为了便于维护,ExactVM没有从Sun JDK 1.1的Classic VM继承用汇编写的解释器,反而退回到更早版本的用C写的简单解释器实现。

Sun Labs的论文提到JBE的历史:Mixed-mode bytecode execution

Our optimizing compiler traces its heritage back to a vectorizing and parallelizing compiler for Fortran and C developed at Supercomputer Systems Inc. (SSI) during the years 1987-93. Later, a Chaitin-Briggs-style global register allocator was added at Sun Microsystems. Later still, a front-end for Java class file bytecode (henceforth, Java bytecode) was developed and the compiler was integrated into our JVM.

这里的SSI指的是Steve Chen的Supercomputer Systems, Inc,源自Cray。SSI公司在IBM的资助下只活了几年——1987-1993——然后因产品研发进度太慢失去了资助而倒闭。期间SSI不但积极研发新的超级计算机,也配套开发了高度优化的Fortran和C编译器,主攻自动向量优化和并行优化。同一时期Sun也在积极开发C和Fortran编译器,而且似乎有跟SSI合作(Supercomputer Systems Limited Partnership?)。SSI倒闭后,Sun吸收了不少SSI的编译器工程师,并将SSI的编译器技术(后来叫做“UBE”,Unified Back-End)整合到了Sun的C、C++、Fortran、Pascal和Ada编译器中,所谓Sun Studio Compilers。

Sun Studio Compilers这些编译器有各自的前端,但都共用同一个后端;前端生成出后端的IR,“Sun IR”,剩下的优化、代码生成的活都交给后端解决。“鲸书”(《Advanced Compiler Design and Implementation》)有简短提到Sun IR的设计。这个IR是双向链表构成的线性IR,结合了一些高层IR和底层IR的特性,所以其抽象程度被归类为“中层IR“(MIR)。UBE并没有被整合在Sun Studio Compilers的核心中,而是作为这套编译器的x86后端使用。JBE就是UBE为Java裁剪的版本。

JBE把zhe的代码拿进来,稍做裁剪,并且新写了一个Java字节码的前端,搞定!原本这个公共后端里就有许多牛逼的优化,包括当时还比较新潮的基于SSA的优化和优化编译器标配的图着色寄存器分配器,要啥优化随便挑啊。

<- 不不,没那么快。由于Java要支持GC,一些相关功能必须在JIT编译器的IR层面得到体现,例如说

  • 一条对写内存的IR指令,如果是用于实现Java的putfield并且类型是引用类型,那么为了支持分代式GC或者并发GC就需要放write barrier;
  • 在某些位置的IR要记录为检查是否要进入GC的“安全点”(safepoint);
  • 某个位置的IR是否要假设可能会遇到异常。Java的异常处理模型跟C++有点相似但又不一样,原本Sun compiler的IR应该得调整过才能应用于Java。

这些功能在C、C++、Fortran的编译器上不会有,所以JBE把它们得新加进IR里。然后还可以借助一些Java语义做些特定优化,例如说Java不允许指向对象内部的指针;Java里两个数组引用如果不相等,那么它们所指向的数组实例一定不会有部分重叠(overlap),这些特性利用好有助于编译器的别名分析。


然后,JBE毕竟是动态编译器,即便在后台编译比在前台的JIT编译可以容忍更长的编译时间,能忍受的程度还是远不如静态编译器。所以原本在静态编译器里的优化还是得做一些裁剪。

这么一来,JBE的编译器后端就跟原来其它Sun编译器的公共后端越来越不一样,也无法一起维护;JBE只能fork了公共后端的源码然后自己维护…维护过一大坨“别人的代码”而且还是“不断在变的别人的代码”的人都知道这是什么状况。:-( 所幸JBE项目组里的几位主要开发就来自SSI,对这块代码非常熟悉,想必比别人维护要轻松些吧。

更悲剧的是,整个ExactVM项目很快陷入了Sun的内部政治斗争——对手是“外来”的HotSpot VM项目。一山不容二虎,ExactVM与HotSpot VM的技术特性实在太相似,Sun无力支持两个效果几乎一样的Java SE JVM项目,必须砍掉一个。于是两组人斗得个人仰马翻昏天黑地,最终HotSpot VM胜出,顺带从ExactVM那边吸收一些优秀的功能,例如GC接口与CMS GC实现等。

竞争失败后,ExactVM被扔回到labs那边,改名为“Sun Microsystems Laboratories Virtual Machine for Research”(ResearchVM)。名字长到爆,但剩下的生命却甚短…没过多久它的职能就被新的Maxine VM所替代。烧香。


HP JVM / JIT2.0 / ARC

继续盘点大公司。接下来看个HP的故事。

ARIES是Automatic Re-translation and Integrated Environment Simulation的缩写,也有文档说是Automatic Recompilation and Integrated Environment Simulation。后来大家更多就直接叫它Aries而不管原本是啥的缩写了,所以有文档有岔子大概也不奇怪…


关于ARIES的介绍,请参考:ARIES Technical OverviewAries: Transparent Execution of PA-RISC/HP-UX Applications on IPF/HP-UX

简单说,ARIES是一个把HP的PA-RISC机器码动态翻译为Itanium机器码的动态二进制翻译器。“二进制翻译器”是从虚拟机的角度的叫法;其实它底下的技术有许多与编译原理共通的地方。现代trace-based编译器的鼻祖就是这些二进制翻译器。

说了半天,JVM呢?JIT编译器呢?

HP从Sun购买了Java的授权,以Sun Classic VM为基础开发能运行在HP-UX系统上的JVM。一开始主要工作就是移植,把Classic VM平台相关的部分移植到新操作系统和新硬件上。但是当时Sun提供的sunwjit性能实在差,有几个HP工程师看不下去了,提议开发一个新的、trace-based JIT编译器,名为“JIT2.0”。我不太清楚这里的时间顺序是怎样,JIT2.0项目使用了ARIES二进制翻译器的技术,后来进一步成为“ARC”(Adaptive Run-time Compiler)。可以从其相关专利一窥究竟:

Patent US7725885: Method and apparatus for trace based adaptive run time compiler

(以前我一直以为近年来流行的trace-based编译技术是Andreas Gal从以前的动态二进制翻译技术得到灵感应用在JIT编译器上,然后才带起潮流;知道了HP早在90年代末2000年代初就在产品里应用上了trace-based编译技术我还真是吃了一惊。)

可惜,JIT2.0/ARC又是死在HotSpot VM的手上。

HP开发JIT2.0/ARC大概在Sun JDK 1.1.x-1.2.x时代,而Sun当时紧接着就准备推出高性能的HotSpot VM取代Classic VM作为新的默认JVM实现。HP拿到HotSpot VM的早期版本评估其性能时,发现它比Classic VM快了很多;即便Classic VM搭载上JIT2.0/ARC性能还是远不如HotSpot。此时HP既可以选择继续优化Classic VM,找出性能问题点并逐一修补,也可以选择抛弃之前的工作改用Sun的新JVM。权衡一番,HP决定结束一切在Classic VM上的开发,赶紧转向基于HotSpot VM继续开发。基于Classic VM的JIT2.0/ARC项目就此被终止。顺带一提,微软和IBM都是选择了走“魔改Classic VM“的路,效果也不差。

更可悲的是,后来人们看回这段历史,发现当时HP做性能评测没有意识到其实在那些测试里Classic VM是败在GC性能比HotSpot VM差太远,而不是败在JIT编译器太差。本来很有潜力的trace-based JIT编译器先驱就这么埋没了。诶。


今天先写到这里,下一篇继续看看各个JIT编译器的血缘的故事。敬请期待 :-)

(题图引用自Optimizing C++ Code : Overview

24 条评论