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