1. 程式人生 > >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翻譯到機器碼。

可以看到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寫的簡單直譯器實現。

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是一個把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編譯器的血緣的故事。敬請期待 :-)