Tencent JDK 國產化CPU架構支持分享
GIAC(GLOBAL INTERNET ARCHITECTURE CONFERENCE)是長期關注互聯網技術與架構的高可用架構技術社區和msup推出的,面向架構師、技術負責人及高端技術從業人員的年度技術架構大會,是中國地區規模最大的技術會議之一。
本文引用地址:http://www.104case.com/article/202009/418153.htm今年的第六屆GIAC大會上,在大數據架構進化中的JAVA專題,騰訊高級工程師傅杰博士發表了《Tencent JDK 國產化CPU架構支持分享》的主題演講。以下為嘉賓演講實錄:
尊敬的各位來賓,大家下午好!很高興有機會跟大家一起分享Tencent JDK 國產化CPU架構支持的話題。我是來自騰訊JVM團隊的jiefu(傅杰),在中科院計算所碩博連讀期間開始從事OpenJDK的研發工作,目前是OpenJDK社區的committer。我曾就職于龍芯,是OpenJDK mips分支的核心開發者,在龍芯上開拓并實現了OpenJDK的C2編譯器。加入騰訊后,主要致力于KonaJDK在大數據和機器學習等領域的探索和實踐。
今天,我首先向大家簡單介紹一下Tencent Kona JDK;隨后,詳細闡述JVM對國產CPU體系結構的支持;最后,和大家一起探討處理器內存模型對JVM實現的影響。
Tencent Kona JDK簡介

Tencent Kona是騰訊基于OpenJDK研發的一款JDK產品,于2019年免費對外開源,并提供長期支持(LTS)。Kona的每個發布版本都經過了騰訊云和內部實際生產環境的測試驗證,歡迎大家下載使用。

2020年3月JDK14發布時,我司是國內有限的若干公司,進入全球突出貢獻者/組織名單。OpenJDK全球貢獻者榜單是對全世界各個公司或個人對OpenJDK貢獻的權威統計,由Oracle在新版本JDK發布時對外公布。
騰訊的JVM團隊(含多位OpenJDK社區的 author/committer),專門負責Kona的研發和維護。僅最近半年時間,團隊已向OpenJDK社區貢獻了幾十個修復Bug的patch。同時鵝廠也將自身海量生產負載經驗和前沿實踐,貢獻給OpenJDK社區。未來,我們將以更加開放的姿態積極擁抱開源,并持續貢獻開源。
JVM對國產CPU體系結構的支持

下面跟大家分享JVM對國產CPU體系結構支持的相關內容。國產處理器是我國發展信創產業的根基。目前,進入官方名錄的國產處理器按架構可分為ARM、MIPS、Alpha和X86四大架構。其中,ARM以鯤鵬和飛騰為代表,MIPS以龍芯為代表,Alpha以申威為代表,X86則以兆芯和海光為代表。上述四種架構,除ARM和X86有OpenJDK社區支持外,MIPS和Alpha均無社區支持,全部需要自行開發和維護。因此,掌握JVM對處理器支持的技術,對于打破外國壟斷、促進國產處理器持續健康發展具有十分重要的意義。

OpenJDK的HotSpot虛擬機是全世界應用最廣的高性能Java虛擬機。從宏觀設計層面,HotSpot虛擬機可分為類加載器、運行時、執行引擎和垃圾收集器四個模塊。其中,只有執行引擎和處理器體系結構密切相關,其它三個模塊幾乎平臺無關(或僅部分與操作系統相關,如運行時模塊)。JVM的執行引擎負責將Java字節碼轉換為處理器硬件支持的機器指令,故該模塊絕大部分與CPU相關。因此,JVM對國產化處理器體系結構的支持,本質上是要實現國產化處理器上的JVM執行引擎。那么,JVM的執行引擎在代碼層面又該如何落地實現呢?

這頁PPT的左邊部分展示了HotSpot虛擬機源代碼組織結構。按與底層硬件和操作系統的相關性,HotSpot源代碼分為cpu(處理器相關)、os(操作系統相關)、os_cpu(處理器和操作系統同時相關)和share(平臺無關)四個子目錄。PPT中間部分列舉了各個子目錄實現的主要功能,其中標黃色的部分為CPU體系結構相關部分。PPT右側以ARM的aarch64處理器架構為例,量化分析了JVM支持一款處理器架構所需的代碼量,其中CPU體系結構相關的代碼量約為64000行,剩余部分的代碼量約為70萬行。故處理器體系結構支持所需的代碼占比小于8%。體系結構相關代碼主要包括匯編器、解釋器和編譯器后端。此外,由于Java語言原生支持多線程,故還需要處理器提供原子操作和內存屏障,以保證并發程序的正確性。下面我們將從匯編器、解釋器、編譯器、CPU原子操作和內存屏障這幾個方面逐一展開。
匯編器是第一個需要實現的模塊,因為解釋器和編譯器的構造均依賴于匯編器提供接口。匯編器主要對處理器硬件進行抽象和封裝,向上提供編程所需的寄存器和指令。匯編器是幾個模塊中功能最簡單的。但從工程實現上看,由于現代處理器動則支持幾千條指令,故匯編器的實現任務繁重,且指令格式和編碼稍有不慎很容易引入錯誤。因此,要求開發人員熟悉處理器指令集,并且在編碼過程中務必小心謹慎。
匯編器完成后,緊接著需要實現解釋器。問大家一個問題:能不能跳過解釋器,直接實現HotSpot虛擬機的編譯器?有人覺得解釋器性能太低,想剔除解釋器模塊,以減少JVM對CPU架構支持的工作量。答案是否定的。HotSpot虛擬機必須依賴解釋器的功能。首先,對部分特殊的Java方法(如體積超大),編譯器會拒絕編譯,只能由解釋器解釋執行。其次,HotSpot的編譯器,尤其是C2編譯器,大量使用基于某些假設的激進編譯優化。但這些假設并不總是成立的,一旦失效,虛擬機需要由編譯執行回退到解釋器繼續執行。最后,在某些要求快速啟動和響應的場景,直接解釋執行的可能會更優于先編譯再執行。因此,對解釋器的構建和支持是必須的。

HotSpot的解釋器為基于模板的高性能解釋器。所謂的“模板”,即一段用于實現Java字節碼語義功能的匯編指令序列。這頁PPT展示了add方法被javac編譯為四條字節碼,然后再被解釋執行的過程。解釋執行,其實就是按程序的控制流,逐一執行字節碼對應模板中指令序列的過程。PPT的右邊展示了整數加法iadd字節碼的解釋器模板。上面黃色虛線框中的機器指令用于取操作數。下面黃色虛線框中的機器指令用于跳轉到下一個字節碼對應的模板繼續執行。中間的一條add加法指令用于實現iadd字節碼的語義。解釋器的模板都遵循一個固定模式,即先取操作數,然后執行,最后跳轉到下一個模板繼續運行。
解釋器調試成功之后,就可以開始編譯器的支持了。編譯器支持難度最大,調試周期也最長。HotSpot中設計了C1和C2兩款編譯器。C1編譯器編譯速度快,但生成的代碼質量不高,適用于要求快速啟動和響應的場景,因此又被稱為client版編譯器。C2編譯器生成的代碼質量高,但編譯速度慢,適用于需要長期反復執行的服務類應用,因此又被稱為server版編譯器。相對于C1,C2采用了更多和更激進的編譯優化算法,故C2比C1更復雜。C1和C2的構造有許多相通之處,下面我們以復雜度更高的C2為例,向大家展示如何在JVM上實現一款支持新CPU架構的編譯器。

這頁PPT展示了C2編譯器構造的原理。為了降低編譯器移植難度,C2被劃分為平臺無關和平臺相關兩個部分。平臺無關的代碼對所有處理器架構都適用,僅平臺相關部分的代碼需要對處理器架構進行移植適配。進一步地,為了減少人工編寫平臺相關部分代碼的工作量,C2借助ADL編譯器來自動生成處理器體系結構相關的代碼。ADL是Architecture Description Language的英文縮寫,是內嵌于OpenJDK開源代碼中的體系結構描述語言。ADL編譯器通過解析體系結構描述文件(以*.ad為后綴的文件,例如aarch64.ad)來生成C2代碼。故在新處理器架構上支持C2的大部分工作,是正確編寫處理器的體系結構描述文件。體系結構描述文件主要涉及寄存器描述、操作數描述和指令集描述三大方面的內容。

這頁PPT以Aarch64為例展示了寄存器描述的實例。寄存器描述通常包括通用寄存器、浮點寄存器和向量寄存器。為了兼容32位操作系統,寄存器描述時以32位長度為基本描述單元。例如,PPT上半部分的R1和R1_H聯合起來表示64位的R1寄存器。PPT下半部分的V0、V_H、V_J和V_K聯合起來表示128位長度的V0浮點寄存器。

這頁PPT展示了操作數描述的實例。操作數描述處理器直接支持的數據種類,包括立即數操作數、寄存器操作數和存儲器操作數三大類別。在每個大的類別中,又會進一步細分為字符型、整型、浮點型和指針等具體的子類型。

這頁PPT展示了指令描述的實例。需要提醒大家注意的是,指令描述不光描述處理器硬件支持哪些指令,同時還會影響C2編譯器的指令選擇和生成,從而影響編譯器性能。實際上,體系結構文件中的指令描述規定了如何用CPU的機器指令去匹配編譯器的中間代碼表示。PPT左側addI_reg_reg的指令描述,會匹配編譯器中間代碼表示的AddI節點及其操作數src1/src2,如PPT右圖所示。
寄存器、操作數和指令描述都完成后,JVM對CPU架構的支持已接近尾聲了。此時,大家千萬不要忘記了還有之前提到的CPU原子操作和內存屏障。如下頁PPT所示,HotSpot中定義了非常清晰的原子操作和內存屏障接口,大家只需根據處理器特性逐一實現即可。原子操作大家都很熟悉,那什么是內存屏障呢?下一節我會為大家詳細介紹。
處理器內存模型與JVM實現
下面跟大家一起探討處理器內存模型對JVM設計的影響。為什么將這個話題單列出來呢?多年的實踐經驗告訴我們,JVM實現最考驗工程師水平的就是處理器內存模型與JVM的適配。這部分工作決定了虛擬機能否在處理器上穩定運行。希望能引起大家的重視。
處理器內存模型存在強弱之分。強內存模型以X86為代表;弱內存模型以ARM和PowerPC架構為代表。那么處理器內存模型的強弱是如何定義的呢?下面這張PPT展示了內存模型強弱劃分的依據:按處理器允許訪存指令重排序的多少來劃分。一般地,允許訪存指令重排序的情形越多,處理器內存模型越弱,反之越強。訪存指令分為讀(Load)和寫(Store)兩種操作。因此,可能的重排序情形包括讀讀(Load/Load)、讀寫(Load/Store)、寫讀(Store/Load)和寫寫(Store/Store)重排序。X86架構處理器僅允許寫讀(Store/Load)重排序,而ARM和PowerPC對上述四種重排序均允許。故X86通常被認為是強內存模型,而ARM和PowerPC被認為是弱內存模型。
然而,我們在編程時,尤其是在并發程序設計時,可能需要禁止處理器的重排序行為。這時就需要借助處內存屏障來完成。所謂的“內存屏障”,是指處理器硬件支持的、專門用于禁止特定訪存指令重排序的機器指令。如下頁PPT所示,HotSpot虛擬機針對四種可能的重排序情形,提供了對應的內存屏障接口。例如,如果希望禁止X86處理器的寫讀重排序,只需要調用OrderAccess::storeload()這個內存屏障接口即可。除了上述四種基本的接口外,虛擬機中還定義了acquire、release和fence接口。其中,acquire可禁止讀讀和讀寫重排序,release可以禁止讀寫和寫寫重排序,fence則禁止所有重排序。

編譯器在指令生成階段需充分適配處理器的內存模型特性。下面的PPT展示的是C2編譯器MemBarStoreStore中間節點,在X86架構和Aarch64架構上目標代碼的生成情況。MemBarStoreStore中間節點的語義是禁止處理器的寫寫重排序。由于X86的內存模型不允許寫寫重排序,故該中間節點在X86架構上無需生成額外機器指令即可保證語義正確。而Aarch64架構處理器本身允許寫寫重排序,故需要額外生成一條寫寫的內存屏障才能正確實現該節點語義。一般地,弱內存模型架構通常需要生成更多的內存屏障。

如果JVM對處理器訪存模型適配不當會發生什么呢?肯定會引起Bug。此類Bug通常具有隨機性、位置發散和表象多樣等特點,分析和調試難度很高。下面跟大家分享一個自己解決的OpenJDK訪存模型適配不正確的Bug(JDK-8229169)。這個Bug在jdk14中首先被修復,隨后也被backport到了jdk8和jdk11等LTS版本。

該Bug位于HotSpot垃圾收集框架的任務竊取(work stealing)階段,影響除串行GC以外的所有垃圾收集器。Bug的機理是處理器在執行GenericTaskQueue::pop方法時,對_age的兩次讀操作(見下頁PPT中黃色字體所示)被處理器亂序了。解決方法是在兩個讀操作之間添加讀讀內存屏障(PPT中綠色字體所示),以禁止處理器的讀讀亂序??赡苡腥藭枺河捎赬86處理器不允許讀讀亂序,故在X86上可以不用添加這個內存屏障,為何不采用PPT右下角的修改方式呢?這個問題的正確答案是X86也需要添加OrderAccess::loadload()進行修復。這是因為雖然X86在執行時不會對讀讀操作重排序,但是編譯器在編譯這段代碼時可能會發生重排。為了禁止代碼在編譯階段被重排序,X86也需要這個patch。從上述分析不難看出,JVM中的OrderAccess訪存屏障同時具備禁止處理器和編譯器重排序的功能。這一點請大家在今后的開發過程中多多注意。

以上就是我今天跟大家分享的內容。謝謝大家!另外,歡迎大家關注和star Tencent Kona JDK 8
評論