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。這是個編譯速度非??斓木幾g器,完全不做優化,甚至連代碼校驗(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”,剩下的優化、代碼生成的活都交給后端解決?!蚌L書”(《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 Overview,Aries: 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)??梢詮钠湎嚓P專利一窺究竟:
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繼續開發?;贑lassic VM的JIT2.0/ARC項目就此被終止。順帶一提,微軟和IBM都是選擇了走“魔改Classic VM“的路,效果也不差。
更可悲的是,后來人們看回這段歷史,發現當時HP做性能評測沒有意識到其實在那些測試里Classic VM是敗在GC性能比HotSpot VM差太遠,而不是敗在JIT編譯器太差。本來很有潛力的trace-based JIT編譯器先驅就這么埋沒了。誒。
?
今天先寫到這里,下一篇繼續看看各個JIT編譯器的血緣的故事。敬請期待 :-)
(題圖引用自Optimizing C++ Code : Overview)
from:https://zhuanlan.zhihu.com/p/19954031?
總結
以上是生活随笔為你收集整理的JIT编译器杂谈#1:JIT编译器的血缘(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入理解多线程(五)—— Java虚拟机
- 下一篇: 深入理解JVM之前端编译器(一)