OpenJDK 項目 Valhalla 發佈 LW2 原型

Oracle OpenJDK團隊已經發布了Valhalla項目LW2原型的早期訪問(EA)構建版本(也稱爲“內聯類”,以前稱爲“值類型”)。

該原型可以在這裏下載,其目的是在未來幾周內定期通過Bug修復和性能升級來更新二進制文件。

團隊正在積極地尋找關於用戶模型的反饋,並提醒說,該實現的許多主要方面還沒有準備好接受檢查。

LW2的命名錶明內聯類特性的實現已經達到了“L-World”設計的第二個里程碑。

當前原型通過使內聯類型儘可能地與現有對象和接口(即L-Type系統或“L-World”)相似,將內聯類型合併到現有類型系統中。

該原型於2019年7月5日發佈,取代了之前的LW1里程碑。它將Valhalla和內聯類的初步實驗提供給更多的開發人員,儘管它仍然是非常實驗性的。

它還邁出了重新審視泛型的第一步——其語法允許將內聯類的可空引用投影(projections)用作泛型類型的參數。

由於這是一個早期原型,有一些限制,包括:

  • 僅在x64 Linux、x64 Mac OS X及x64 Windows上可用;
  • 不支持包含間接類型的原子字段;
  • 不支持@Contended內聯類型字段;
  • 只支持解釋器和C2,不支持C1,沒有分層編譯,也沒有Graal;
  • 內聯類型沒有不安全字段和數組訪問器API;
  • 解釋器還沒有優化,其重點是C2 JIT優化。

InfoQ就LW2的發佈採訪了Dan Heidinga(IBM Eclipse OpenJ9項目負責人)。

InfoQ:您認爲LW2中最重要的新特性是什麼?

Dan Heidinga:LW2早期訪問構建給我們帶來了很多東西,其中最重要的是內聯類型的用戶模型。以前的原型是用MethodHandles來定義的,有很高的進入門檻。

使用以前的原型編寫重要的代碼太困難了,即使對於MethodHandle專家來說也是如此,這使得提供反饋非常困難。LW2不同。它的目的是對用戶友好,以便開發人員能夠測試模型,瞭解內聯類型如何工作,並向專家組提供反饋。

一個特別重要的特性是內聯類型運算符“?”的引入,它允許用戶顯式地選擇內聯類型是要內聯的候選類型,還是應該使用間接類型。

它還支持將內聯類型與Java的集合庫一起使用,這再次提高了開發人員使用原型的體驗。只允許泛型使用“?”或者LW2中類型的可空版本,我們爲將來的改進留有餘地,比如內聯類型的具體化泛型。

InfoQ:您能解釋一下內聯類與逃逸分析的關係嗎?爲什麼這對性能很重要?

Heidinga:逃逸分析是一種JIT優化,它試圖證明一個對象的生命週期完全包含在當前的編譯單元中,不會“逃逸”到堆、另一個線程,甚至不會超出當前內聯方法集的範圍。

如果JIT能夠證明對象沒有逃逸,那麼它就可以將對象分割成一組獨立的字段。然後可以將它們放入寄存器中,以獲得更好的優化機會,或者可以在棧而不是堆上分配對象。這兩種方法都提供了更好的優化機會,並減少了垃圾收集的壓力,因爲對象永遠不會結束在堆上。

雖然這聽起來很棒,但並無保證。優化可能會失敗,原因有很多——比如沒有在方法中內聯足夠多的調用以確保對象真得沒有逃逸,或者在給定的編譯操作中可能無法運行。這可能是因爲較低層的編譯可能會跳過一些昂貴的優化以獲得更快的代碼編譯速度。不僅如此,對代碼的微小更改可能會導致對象逃逸,使優化無法成功,從而導致性能下降,而又沒有明顯的原因。

內聯類是不可變的,沒有標識。這使得這些類型成爲逃逸分析的理想候選類型,因爲JIT不再需要證明它們沒有逃逸。它可以自由地將它們分割、放入寄存器並優化它們,但是它需要這樣做,因爲它總是可以在任何可能逃逸的位置重新構造內聯類型。這裏的關鍵是,由於內聯類型沒有標識,所以無法判斷它們是否被重新創建。這消除了傳統逃逸分析中的許多脆弱性。

在大多數程序中,有許多小類充當其他數據的封裝器,這些小類將受益於這種有保證的逃逸分析。考慮下代碼中封裝int、long或String以賦予它們額外語義的位置。如果JIT能夠棧分配所有這些實例不是很好嗎?這是內聯類型取得成功的部分原因。

InfoQ:內聯類的實例是不可變的,所以您能解釋一下爲什麼這會導致更新原子性問題嗎?爲什麼我們不能使用樂觀副本和比較與交換(CAS)來交換指針呢?

Heidinga:請記住,項目的口號是“讓和類一樣的代碼像int一樣工作”。當將一個基本的int類型寫入局部變量、字段甚至數組時,我們不修改int類型。相反,我們覆蓋了全部內容。LW2的內聯類就是以這種方式像基本類型一樣工作。

雖然這在概念上是一個乾淨的模型,但是它重新引入了一個自從64位系統成爲常態以來Java開發人員一直都可以忽略的問題:分裂(tearing),也就是非原子更新。

回顧早期32位Java實現處理long或double的方式有助於理解這個問題。CPU保證其本機字大小的寫操作是自動發生的。在一個32位系統上寫32位是不會分裂的。但long是64位的,這意味着它不能在沒有向硬件發送特別請求的情況下在32位系統上自動寫入。

Java語言規範在“17.7. Double和long的非原子處理”這一節中承認了這一點:

就Java編程語言內存模型而言,對非易失性long或double值的一次寫操作被視爲兩次單獨的寫操作:

一次32位。這可能導致這樣一種情況:一個線程從一次寫入中看到了這個64位數值的前32位,從另一次寫入中看到後32位。

內聯類會將分裂問題重新帶回到用戶需要關注的問題集裏,因爲內聯類型的讀寫必須複製該類型的全部內容。

這裏有一個例子來幫助理解爲什麼內聯類的分裂對開發人員來說是一個新問題。

考慮內聯類Customer:

    inline class Customer {
        String firstName;
        String lastName;
        long customerID;
    }

以及一個用於跟蹤前三名客戶的數組:

    Customer topCustomers[3];

該數組由兩個線程併發訪問。第一個線程嘗試將一個新客戶寫入數組:

    Customer c = getTopCustomer();
    topCustomers[0] = c;

同時,第二個線程則嘗試讀取數組:

    Customer b = topCustomers[0];

如果讀和寫同時發生,則有可能讀取到一個新老客戶合併而成的客戶,從而產生一個不可能的客戶對象。這是一次數據競爭,但在此之前(大部分情況下)是良性的,因爲運行時一直在數組中原子性地用一個指針替換另一個指針。

一旦內聯類型大於處理器提供的最大原子更新(通常是字長的兩倍,所以64位系統上是128位),那麼分裂就成爲一個潛在的問題——如果內聯類型上有任何數據競爭的話。內聯類型太大,CAS無法成功,因爲它需要更新整個內容,而不僅僅是指向它們的指針。由於該類型的所有內容都內聯在容器中,所以沒有方便更新的指針。

Valhalla專家組仍在研究如何將內聯類型標記爲“原子類型”,這樣它們就只能以不可分裂的方式寫入,這將解決其中的一些問題,但是LW2目前還沒有包含這個建議。

內聯類型的設計建議,內聯類型應該是很小的數據聚合(此處的關鍵字是“小”),分裂是提出這項建議的另一個原因。

InfoQ:您認爲目前開發人員社區對內聯類的最大誤解體現在哪一方面?

Heidinga:我將給出兩個答案。第一個常見的誤解是內聯類型是可變的,就像C的結構一樣。雖然IBM的PackedObjects或Azul的ObjectLayout等早期建議支持可變類型,但LW2的內聯類型嚴格來說是不可變的。這是Java新特性的總體趨勢的一部分,它支持不變性,以便更容易編寫正確的併發應用程序。

第二個常見的誤解是內聯類型讓用戶顯式地控制對象的佈局。內聯類型允許用戶要求JVM將數據直接內聯到容器中(對象或數組),但不允許用戶控制類型中字段的佈局。佈局算法仍然完全在JVM的控制之下,JVM可能會對字段進行重新排序,從而對它們進行分組,從而提高垃圾收集的效率。

InfoQ:您還有其他什麼想和我們的讀者分享嗎?

Heidinga:在LW2早期訪問二進制文件和經過更新的JVM規範中有很多內容。請查看並給我們提供反饋。在設計中有許多打開的問題,我們期待你關於該設計在你的用例中表現如何的經驗報告。

我們正在尋求反饋的一些領域包括==對於內聯類型的行爲、在Object[]和內聯類型數組之間引入的數組協方差,以及你在你的代碼庫中試驗內聯類型時的體驗。

Valhalla項目的LW2二進制文件現在已經發布,並且正在積極地尋求來自普通Java開發人員的反饋(在已經準備好的領域,例如用戶模型)。

原文鏈接:

OpenJDK Project Valhalla Releases LW2 Prototype

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章