[新聞] CPython / 微軟 Pyjion / IBM Python+OMR
大背景
在進入主題前,請先看看IBM Research以前做過的類似項目的經驗:Fiorano項目。
Fiorano是IBM Research做的一次嘗試,將IBM J9 JVM所使用的Testarossa(TR)編譯器單獨拿出來,插入到CPython運行時中為後者提供JIT編譯服務。
傳送門:
- Adding Dynamically-Typed Language Support to a Statically-Typed Language Compiler: Performance Evaluation, Analysis, and Tradeoffs
- On the Benefits and Pitfalls of Extending a Statically Typed Language JIT Compiler for Dynamic Scripting Languages
結果呢?當然Fiorano沒有被整合到官方CPython里,不然現在大家就已經在用它了。但作為研究性項目它還是有點意思的——我覺得最重要的一點,是在一個原本沒有打算與高性能JIT編譯器搭配使用的runtime上,很難實現出特別有效的優化。在主流JVM上,JIT編譯後的代碼的速度可以輕易達到解釋器速度的10x水平;而Fiorano帶上了JIT卻也就達到了純解釋執行的CPython的速度的1.2x~2.74x的水平範圍,並沒有給大家帶來多少震撼…
Pyjion
項目地址:GitHub - Microsoft/Pyjion: Pyjion
開源許可證:MIT
是的,微軟近期也加入了給CPython加JIT編譯器的大混戰。微軟甚至還有一個寄身於Data Group in Azure組的Python研發組,最近開始對外宣傳。
項目名是「Pyjion」,讀作「pigeon」(鴿子),因為項目主力成員Dino大大想要有Python的Py音節、JIT的Ji音節的詞…就找(sheng)到(zao)了這麼個詞出來。GJ!
說起項目主要成員之一的Dino Viehland大大,他以前是IronPython與DLR的主力開發之一,後來也參與了Python Tools for Visual Studio(PTVS)的開發。大家用Visual Studio / VS Express開發Python爽不?裡面就有Dino大大的功勞。
可見他對Python那可是有深深的怨念…是真愛啊!
而Pyjion項目的另一個主要成員是Brett Cannon。他從2003年開始就是CPython的core commiter了。這也是真愛啊!
未來傳送門:
- PyCon US 2016:Presentation: Pyjion: who doesn』t want faster for free? - 2016-05-30
言歸正傳,這Pyjion到底是啥呢?它是Brett和Dino做的實驗產物,為了在保持完全兼容的前提下提升CPython的性能。目前基於的CPython版本是3.6 alpha 1。
項目官網的一句話說明是:"A JIT for Python based upon CoreCLR"。它目前的項目目標有三個:
- Add a C API to CPython for plugging in a JIT(代碼) <- 最主要的目標
- Develop a JIT module using CoreCLR utilizing the C API mentioned in goal #1(代碼) <- 概念驗證用
- Develop a C++ framework
目標1很簡單,就是給CPython添加一組新的C API及其實現,來為外部的JIT編譯器提供接入CPython運行時的鉤子。這部分目前設計和實現都很直觀,看看上面的代碼鏈接的patch就知道它是啥了——在解釋器入口處添加鉤子,當有JIT編譯器註冊進來時,一個函數在即將開始被解釋執行時會先嘗試JIT編譯,如果成功以後就執行JIT出來的機器碼;如果不成功就會把該函數標記為不可JIT編譯,以後就不再嘗試了。
目前這C API並不太靈活,只允許以Python函數為單元來編譯,編譯必須對整個函數成功,否則就得整個函數留在解釋器里跑。這個API沒有考慮到在函數中間跳進JIT編譯的代碼(On-Stack Replacement,OSR)或從JIT編譯的代碼中途跳回到解釋器(deoptimization)之類的需求。
目標2的描述方式挺有趣的:把CoreCLR當作JIT編譯器插入CPython。啥?難道為了JIT還得把整個CoreCLR都拉進來么?太可怕了!
實際上當然沒那麼糟糕。這個描述方式感覺是故意說得模糊一些。其實Pyjion只是要使用CoreCLR裡帶著的RyuJIT編譯器來為CPython服務。但是當前的RyuJIT的實現依賴了CLR / CoreCLR提供的JIT編譯器介面,所以要單獨使用RyuJIT的話,得要把原本由CLR / CoreCLR提供的一些服務/介面給模擬出來才行。這個模擬層在Pyjion代碼里就是CExecutionEngine、CorJitInfo等類。
換言之,Pyjion自身在pyjit.dll中,而它並不真的需要依賴整個CoreCLR(主體位於coreclr.dll),而只需要其中的RyuJIT(位於clrjit.dll)及其必須依賴的庫(例如gcinfo),然後提供CExecutionEngine、CorJitInfo等類的實現給RyuJIT模擬出它所依賴CoreCLR的一些功能。
據說RyuJIT其實是希望未來與CLR / CoreCLR分離開,變得更獨立,便於在諸如Pyjion這樣的場景單獨使用。目前RyuJIT與CLR確實不是由同一個組負責開發的,要分家也很合理。但未來會如何發展,外界也只能拭目以待了。
那麼Pyjion是如何使用RyuJIT的呢?
它並沒有實現一個RyuJIT的前端,直接把CPython位元組碼轉換為RyuJIT的IR;而是把CPython位元組碼先轉換為CLR的MSIL位元組碼,然後再讓RyuJIT去把這MSIL編譯成機器碼,最後安裝到CPython運行時里去運行。這種做法或許多少與項目組成員之前做IronPython的經歷有關係,或者是與RyuJIT現在與CLR / CoreCLR的偶和有關係。
不過這裡生成的MSIL只用了MSIL的指令集,而沒有完全實現標準的Assembly格式;其元數據相關部分都是Pyjion用自己的數據結構模擬出來的,所以無法將生成的MSIL交給諸如ildasm之類的工具來查看。
具體的轉換步驟是:
- CPython的解釋器入口PyEval_EvalFrameEx()調用JIT編譯器JitCompile()函數,傳入CPython位元組碼。
- JIT編譯器入口JitCompile()創建AbstractInterpreter與PythonCompiler,調用AbstractInterpreter::compile()開始編譯流程。
- AbstractInterpreter類充當CPython位元組碼的解析器(parser),一邊抽象解釋CPython位元組碼一邊調用PythonCompiler來生成MSIL。
- AbstractInterpreter::preprocess()先把CPython位元組碼里偷懶而設計的"Block"給預處理掉,把循環的跳轉目標、異常處理塊的邊界給找出來並扁平化。可能有同學不理解「偷懶」是什麼意思:Python的位元組碼編譯器在處理循環和異常相關的控制流時,沒有在編譯器里處理嵌套關係,而是把「作用域棧」留到了解釋器里。而正確的做法是在編譯器里處理掉它,例如這樣:如何對C語言的FOR語句給出一個生成中間代碼的語法制導定義? - RednaxelaFX 的回答
- AbstractInterpreter::interpret()遍歷一遍整個CPython函數的位元組碼,找出基本塊邊界、異常處理塊的邊界,以及收集一些後續優化可能用到的信息。例如說它會做個很保守的逃逸分析來判斷哪些值沒有逃逸,後面就可以選擇對它們做進一步特殊優化,例如下文提到的tagged pointer。
- AbstractInterpreter::compile_worker()一個個基本塊遍歷CPython位元組碼並生成MSIL位元組碼。
- PythonCompiler會把每種CPython位元組碼的操作映射為合適的MSIL位元組碼序列。簡單的CPython位元組碼可以直接映射為一條或多條MSIL位元組碼,而複雜的位元組碼則映射為Pyjion的intrinsic函數的調用。
- 例如兩個Python對象相加,會映射為對Pyjion提供的「PyJit_Add()」函數的調用,而這個函數會調用回到CPython運行時里的實現。
- 具體生成MSIL的有一個ILGenerator類。它與.NET的System.Reflection.Emit.ILGenerator頗為神似。
- PythonCompiler::emit_compile() -> ILGenerator::compile() -> CILJit::compileMethod() MSIL傳入RyuJIT開始編譯。
- 接下來就交給RyuJIT編譯,得到編譯好的機器碼以及一些相關的元數據。
換句話說,Pyjion這種實現JIT編譯的方式,實際的效果是把一個Python函數的位元組碼全部粘合到一起,去掉了解釋器循環自身的開銷,但是大部分複雜的操作還是調用回到CPython運行時去處理的。
要說在語義層面上的優化,Pyjion嘗試了給CPython添加tagged pointer來減少小整數的內存開銷,順帶提高運行性能(因為實際數據就偽裝在指針里,離運算更近了)。但為了保證兼容性,tagged pointer只在被JIT編譯的函數內部使用,一到return_value之類的要暴露(escape)出去的地方就還是裝箱(box)回到原本的對象形態。對應的intrinsic實現在TAGGED_METHOD宏里(例如PyJit_Add_Int()就是這樣來的)。
原本CPython解釋器在解釋執行每N條位元組碼指令後都會做些周期性檢查,例如是否應該釋放GIL來給別的線程機會執行。Pyjion把Python代碼JIT編譯後,這些周期性檢查就安放在用戶代碼里的循環回跳(backedge)的地方。這跟HotSpot VM的JIT編譯代碼選擇的放置safepoint polling的位置一樣。
總體來說,Pyjion採用了一種非常保守的實現方式,很容易保證正確性,但能帶來的性能提升也會非常有限。保守是否就意味著容易被接受呢?難說…搞不好會給人太多想像空間結果很失望orz
希望當前的保守設計只是一個過渡階段。畢竟這個設計比Fiorano的做法還要保守,能帶來的性能提升就更有限了。
在JIT編譯之外,Pyjion還有沒有向CPython注入任何其它東西呢?一點也沒有。GIL、GC、監控之類的額外功能一概沒碰。
更新傳送門:如何看待微軟 Pyjion的進展以及CPython性能優化的未來? - RednaxelaFX 的回答
IBM Python+OMR
回頭一點點更新。先放個傳送門講解背景:如何評價 IBM 的 Ruby + OMR? - RednaxelaFX 的回答
推薦閱讀:
※Flask框架從入門到實戰
※python編程基礎(一)
※配置IPython Notebook提供非本地訪問
※構建PyQt5.8/Python2.7
※Kivy中文編程指南:事件和屬性