Tencent JDK 國產(chǎn)化CPU架構(gòu)支持分享
GIAC(GLOBAL INTERNET ARCHITECTURE CONFERENCE)是長期關注互聯(lián)網(wǎng)技術與架構(gòu)的高可用架構(gòu)技術社區(qū)和msup推出的,面向架構(gòu)師、技術負責人及高端技術從業(yè)人員的年度技術架構(gòu)大會,是中國地區(qū)規(guī)模最大的技術會議之一。
本文引用地址:http://cafeforensic.com/article/202009/418153.htm今年的第六屆GIAC大會上,在大數(shù)據(jù)架構(gòu)進化中的JAVA專題,騰訊高級工程師傅杰博士發(fā)表了《Tencent JDK 國產(chǎn)化CPU架構(gòu)支持分享》的主題演講。以下為嘉賓演講實錄:
尊敬的各位來賓,大家下午好!很高興有機會跟大家一起分享Tencent JDK 國產(chǎn)化CPU架構(gòu)支持的話題。我是來自騰訊JVM團隊的jiefu(傅杰),在中科院計算所碩博連讀期間開始從事OpenJDK的研發(fā)工作,目前是OpenJDK社區(qū)的committer。我曾就職于龍芯,是OpenJDK mips分支的核心開發(fā)者,在龍芯上開拓并實現(xiàn)了OpenJDK的C2編譯器。加入騰訊后,主要致力于KonaJDK在大數(shù)據(jù)和機器學習等領域的探索和實踐。
今天,我首先向大家簡單介紹一下Tencent Kona JDK;隨后,詳細闡述JVM對國產(chǎn)CPU體系結(jié)構(gòu)的支持;最后,和大家一起探討處理器內(nèi)存模型對JVM實現(xiàn)的影響。
Tencent Kona JDK簡介
Tencent Kona是騰訊基于OpenJDK研發(fā)的一款JDK產(chǎn)品,于2019年免費對外開源,并提供長期支持(LTS)。Kona的每個發(fā)布版本都經(jīng)過了騰訊云和內(nèi)部實際生產(chǎn)環(huán)境的測試驗證,歡迎大家下載使用。
2020年3月JDK14發(fā)布時,我司是國內(nèi)有限的若干公司,進入全球突出貢獻者/組織名單。OpenJDK全球貢獻者榜單是對全世界各個公司或個人對OpenJDK貢獻的權(quán)威統(tǒng)計,由Oracle在新版本JDK發(fā)布時對外公布。
騰訊的JVM團隊(含多位OpenJDK社區(qū)的 author/committer),專門負責Kona的研發(fā)和維護。僅最近半年時間,團隊已向OpenJDK社區(qū)貢獻了幾十個修復Bug的patch。同時鵝廠也將自身海量生產(chǎn)負載經(jīng)驗和前沿實踐,貢獻給OpenJDK社區(qū)。未來,我們將以更加開放的姿態(tài)積極擁抱開源,并持續(xù)貢獻開源。
JVM對國產(chǎn)CPU體系結(jié)構(gòu)的支持
下面跟大家分享JVM對國產(chǎn)CPU體系結(jié)構(gòu)支持的相關內(nèi)容。國產(chǎn)處理器是我國發(fā)展信創(chuàng)產(chǎn)業(yè)的根基。目前,進入官方名錄的國產(chǎn)處理器按架構(gòu)可分為ARM、MIPS、Alpha和X86四大架構(gòu)。其中,ARM以鯤鵬和飛騰為代表,MIPS以龍芯為代表,Alpha以申威為代表,X86則以兆芯和海光為代表。上述四種架構(gòu),除ARM和X86有OpenJDK社區(qū)支持外,MIPS和Alpha均無社區(qū)支持,全部需要自行開發(fā)和維護。因此,掌握JVM對處理器支持的技術,對于打破外國壟斷、促進國產(chǎn)處理器持續(xù)健康發(fā)展具有十分重要的意義。
OpenJDK的HotSpot虛擬機是全世界應用最廣的高性能Java虛擬機。從宏觀設計層面,HotSpot虛擬機可分為類加載器、運行時、執(zhí)行引擎和垃圾收集器四個模塊。其中,只有執(zhí)行引擎和處理器體系結(jié)構(gòu)密切相關,其它三個模塊幾乎平臺無關(或僅部分與操作系統(tǒng)相關,如運行時模塊)。JVM的執(zhí)行引擎負責將Java字節(jié)碼轉(zhuǎn)換為處理器硬件支持的機器指令,故該模塊絕大部分與CPU相關。因此,JVM對國產(chǎn)化處理器體系結(jié)構(gòu)的支持,本質(zhì)上是要實現(xiàn)國產(chǎn)化處理器上的JVM執(zhí)行引擎。那么,JVM的執(zhí)行引擎在代碼層面又該如何落地實現(xiàn)呢?
這頁PPT的左邊部分展示了HotSpot虛擬機源代碼組織結(jié)構(gòu)。按與底層硬件和操作系統(tǒng)的相關性,HotSpot源代碼分為cpu(處理器相關)、os(操作系統(tǒng)相關)、os_cpu(處理器和操作系統(tǒng)同時相關)和share(平臺無關)四個子目錄。PPT中間部分列舉了各個子目錄實現(xiàn)的主要功能,其中標黃色的部分為CPU體系結(jié)構(gòu)相關部分。PPT右側(cè)以ARM的aarch64處理器架構(gòu)為例,量化分析了JVM支持一款處理器架構(gòu)所需的代碼量,其中CPU體系結(jié)構(gòu)相關的代碼量約為64000行,剩余部分的代碼量約為70萬行。故處理器體系結(jié)構(gòu)支持所需的代碼占比小于8%。體系結(jié)構(gòu)相關代碼主要包括匯編器、解釋器和編譯器后端。此外,由于Java語言原生支持多線程,故還需要處理器提供原子操作和內(nèi)存屏障,以保證并發(fā)程序的正確性。下面我們將從匯編器、解釋器、編譯器、CPU原子操作和內(nèi)存屏障這幾個方面逐一展開。
匯編器是第一個需要實現(xiàn)的模塊,因為解釋器和編譯器的構(gòu)造均依賴于匯編器提供接口。匯編器主要對處理器硬件進行抽象和封裝,向上提供編程所需的寄存器和指令。匯編器是幾個模塊中功能最簡單的。但從工程實現(xiàn)上看,由于現(xiàn)代處理器動則支持幾千條指令,故匯編器的實現(xiàn)任務繁重,且指令格式和編碼稍有不慎很容易引入錯誤。因此,要求開發(fā)人員熟悉處理器指令集,并且在編碼過程中務必小心謹慎。
匯編器完成后,緊接著需要實現(xiàn)解釋器。問大家一個問題:能不能跳過解釋器,直接實現(xiàn)HotSpot虛擬機的編譯器?有人覺得解釋器性能太低,想剔除解釋器模塊,以減少JVM對CPU架構(gòu)支持的工作量。答案是否定的。HotSpot虛擬機必須依賴解釋器的功能。首先,對部分特殊的Java方法(如體積超大),編譯器會拒絕編譯,只能由解釋器解釋執(zhí)行。其次,HotSpot的編譯器,尤其是C2編譯器,大量使用基于某些假設的激進編譯優(yōu)化。但這些假設并不總是成立的,一旦失效,虛擬機需要由編譯執(zhí)行回退到解釋器繼續(xù)執(zhí)行。最后,在某些要求快速啟動和響應的場景,直接解釋執(zhí)行的可能會更優(yōu)于先編譯再執(zhí)行。因此,對解釋器的構(gòu)建和支持是必須的。
HotSpot的解釋器為基于模板的高性能解釋器。所謂的“模板”,即一段用于實現(xiàn)Java字節(jié)碼語義功能的匯編指令序列。這頁PPT展示了add方法被javac編譯為四條字節(jié)碼,然后再被解釋執(zhí)行的過程。解釋執(zhí)行,其實就是按程序的控制流,逐一執(zhí)行字節(jié)碼對應模板中指令序列的過程。PPT的右邊展示了整數(shù)加法iadd字節(jié)碼的解釋器模板。上面黃色虛線框中的機器指令用于取操作數(shù)。下面黃色虛線框中的機器指令用于跳轉(zhuǎn)到下一個字節(jié)碼對應的模板繼續(xù)執(zhí)行。中間的一條add加法指令用于實現(xiàn)iadd字節(jié)碼的語義。解釋器的模板都遵循一個固定模式,即先取操作數(shù),然后執(zhí)行,最后跳轉(zhuǎn)到下一個模板繼續(xù)運行。
解釋器調(diào)試成功之后,就可以開始編譯器的支持了。編譯器支持難度最大,調(diào)試周期也最長。HotSpot中設計了C1和C2兩款編譯器。C1編譯器編譯速度快,但生成的代碼質(zhì)量不高,適用于要求快速啟動和響應的場景,因此又被稱為client版編譯器。C2編譯器生成的代碼質(zhì)量高,但編譯速度慢,適用于需要長期反復執(zhí)行的服務類應用,因此又被稱為server版編譯器。相對于C1,C2采用了更多和更激進的編譯優(yōu)化算法,故C2比C1更復雜。C1和C2的構(gòu)造有許多相通之處,下面我們以復雜度更高的C2為例,向大家展示如何在JVM上實現(xiàn)一款支持新CPU架構(gòu)的編譯器。
這頁PPT展示了C2編譯器構(gòu)造的原理。為了降低編譯器移植難度,C2被劃分為平臺無關和平臺相關兩個部分。平臺無關的代碼對所有處理器架構(gòu)都適用,僅平臺相關部分的代碼需要對處理器架構(gòu)進行移植適配。進一步地,為了減少人工編寫平臺相關部分代碼的工作量,C2借助ADL編譯器來自動生成處理器體系結(jié)構(gòu)相關的代碼。ADL是Architecture Description Language的英文縮寫,是內(nèi)嵌于OpenJDK開源代碼中的體系結(jié)構(gòu)描述語言。ADL編譯器通過解析體系結(jié)構(gòu)描述文件(以*.ad為后綴的文件,例如aarch64.ad)來生成C2代碼。故在新處理器架構(gòu)上支持C2的大部分工作,是正確編寫處理器的體系結(jié)構(gòu)描述文件。體系結(jié)構(gòu)描述文件主要涉及寄存器描述、操作數(shù)描述和指令集描述三大方面的內(nèi)容。
這頁PPT以Aarch64為例展示了寄存器描述的實例。寄存器描述通常包括通用寄存器、浮點寄存器和向量寄存器。為了兼容32位操作系統(tǒng),寄存器描述時以32位長度為基本描述單元。例如,PPT上半部分的R1和R1_H聯(lián)合起來表示64位的R1寄存器。PPT下半部分的V0、V_H、V_J和V_K聯(lián)合起來表示128位長度的V0浮點寄存器。
這頁PPT展示了操作數(shù)描述的實例。操作數(shù)描述處理器直接支持的數(shù)據(jù)種類,包括立即數(shù)操作數(shù)、寄存器操作數(shù)和存儲器操作數(shù)三大類別。在每個大的類別中,又會進一步細分為字符型、整型、浮點型和指針等具體的子類型。
這頁PPT展示了指令描述的實例。需要提醒大家注意的是,指令描述不光描述處理器硬件支持哪些指令,同時還會影響C2編譯器的指令選擇和生成,從而影響編譯器性能。實際上,體系結(jié)構(gòu)文件中的指令描述規(guī)定了如何用CPU的機器指令去匹配編譯器的中間代碼表示。PPT左側(cè)addI_reg_reg的指令描述,會匹配編譯器中間代碼表示的AddI節(jié)點及其操作數(shù)src1/src2,如PPT右圖所示。
寄存器、操作數(shù)和指令描述都完成后,JVM對CPU架構(gòu)的支持已接近尾聲了。此時,大家千萬不要忘記了還有之前提到的CPU原子操作和內(nèi)存屏障。如下頁PPT所示,HotSpot中定義了非常清晰的原子操作和內(nèi)存屏障接口,大家只需根據(jù)處理器特性逐一實現(xiàn)即可。原子操作大家都很熟悉,那什么是內(nèi)存屏障呢?下一節(jié)我會為大家詳細介紹。
處理器內(nèi)存模型與JVM實現(xiàn)
下面跟大家一起探討處理器內(nèi)存模型對JVM設計的影響。為什么將這個話題單列出來呢?多年的實踐經(jīng)驗告訴我們,JVM實現(xiàn)最考驗工程師水平的就是處理器內(nèi)存模型與JVM的適配。這部分工作決定了虛擬機能否在處理器上穩(wěn)定運行。希望能引起大家的重視。
處理器內(nèi)存模型存在強弱之分。強內(nèi)存模型以X86為代表;弱內(nèi)存模型以ARM和PowerPC架構(gòu)為代表。那么處理器內(nèi)存模型的強弱是如何定義的呢?下面這張PPT展示了內(nèi)存模型強弱劃分的依據(jù):按處理器允許訪存指令重排序的多少來劃分。一般地,允許訪存指令重排序的情形越多,處理器內(nèi)存模型越弱,反之越強。訪存指令分為讀(Load)和寫(Store)兩種操作。因此,可能的重排序情形包括讀讀(Load/Load)、讀寫(Load/Store)、寫讀(Store/Load)和寫寫(Store/Store)重排序。X86架構(gòu)處理器僅允許寫讀(Store/Load)重排序,而ARM和PowerPC對上述四種重排序均允許。故X86通常被認為是強內(nèi)存模型,而ARM和PowerPC被認為是弱內(nèi)存模型。
然而,我們在編程時,尤其是在并發(fā)程序設計時,可能需要禁止處理器的重排序行為。這時就需要借助處內(nèi)存屏障來完成。所謂的“內(nèi)存屏障”,是指處理器硬件支持的、專門用于禁止特定訪存指令重排序的機器指令。如下頁PPT所示,HotSpot虛擬機針對四種可能的重排序情形,提供了對應的內(nèi)存屏障接口。例如,如果希望禁止X86處理器的寫讀重排序,只需要調(diào)用OrderAccess::storeload()這個內(nèi)存屏障接口即可。除了上述四種基本的接口外,虛擬機中還定義了acquire、release和fence接口。其中,acquire可禁止讀讀和讀寫重排序,release可以禁止讀寫和寫寫重排序,fence則禁止所有重排序。
編譯器在指令生成階段需充分適配處理器的內(nèi)存模型特性。下面的PPT展示的是C2編譯器MemBarStoreStore中間節(jié)點,在X86架構(gòu)和Aarch64架構(gòu)上目標代碼的生成情況。MemBarStoreStore中間節(jié)點的語義是禁止處理器的寫寫重排序。由于X86的內(nèi)存模型不允許寫寫重排序,故該中間節(jié)點在X86架構(gòu)上無需生成額外機器指令即可保證語義正確。而Aarch64架構(gòu)處理器本身允許寫寫重排序,故需要額外生成一條寫寫的內(nèi)存屏障才能正確實現(xiàn)該節(jié)點語義。一般地,弱內(nèi)存模型架構(gòu)通常需要生成更多的內(nèi)存屏障。
如果JVM對處理器訪存模型適配不當會發(fā)生什么呢?肯定會引起B(yǎng)ug。此類Bug通常具有隨機性、位置發(fā)散和表象多樣等特點,分析和調(diào)試難度很高。下面跟大家分享一個自己解決的OpenJDK訪存模型適配不正確的Bug(JDK-8229169)。這個Bug在jdk14中首先被修復,隨后也被backport到了jdk8和jdk11等LTS版本。
該Bug位于HotSpot垃圾收集框架的任務竊取(work stealing)階段,影響除串行GC以外的所有垃圾收集器。Bug的機理是處理器在執(zhí)行GenericTaskQueue::pop方法時,對_age的兩次讀操作(見下頁PPT中黃色字體所示)被處理器亂序了。解決方法是在兩個讀操作之間添加讀讀內(nèi)存屏障(PPT中綠色字體所示),以禁止處理器的讀讀亂序??赡苡腥藭枺河捎赬86處理器不允許讀讀亂序,故在X86上可以不用添加這個內(nèi)存屏障,為何不采用PPT右下角的修改方式呢?這個問題的正確答案是X86也需要添加OrderAccess::loadload()進行修復。這是因為雖然X86在執(zhí)行時不會對讀讀操作重排序,但是編譯器在編譯這段代碼時可能會發(fā)生重排。為了禁止代碼在編譯階段被重排序,X86也需要這個patch。從上述分析不難看出,JVM中的OrderAccess訪存屏障同時具備禁止處理器和編譯器重排序的功能。這一點請大家在今后的開發(fā)過程中多多注意。
以上就是我今天跟大家分享的內(nèi)容。謝謝大家!另外,歡迎大家關注和star Tencent Kona JDK 8
評論