在 JVM 中使用透明巨型頁

“[JVM 解剖公園][1]”是一個持續更新的系列迷你博客,閱讀每篇文章一般需要5到10分鐘。限於篇幅,僅對某個主題按照問題、測試、基準程序、觀察結果深入講解。因此,這裏的數據和討論可以當軼事看,不做寫作風格、句法和語義錯誤、重複或一致性檢查。如果選擇採信文中內容,風險自負。


Aleksey Shipilёv,JVM 性能極客   


推特 [@shipilev][2] 

  

問題、評論、建議發送到 [[email protected]][3]


[1]:https://shipilev.net/jvm-anatomy-park

[2]:http://twitter.com/shipilev

[3]:[email protected]


2. 問題


什麼是大內存頁?什麼是THP(透明巨大頁面)?瞭解它能爲我們帶來什麼幫助?


3. 理論


“虛擬內存”概念已經被大家廣泛接受。現在只有少數人還記得"real mode"編程,更不用提實際操作了。在這種模式下編程,會用到實際物理內存。與"real mode"相反,每個進程都擁有自己的虛擬內存空間,虛擬內存空間會映射到實際內存。例如,兩個進程可以在相同的虛擬地址 `0x42424242` 中存儲不同數據,這些數據實際存放在不同的物理內存中。當程序訪問該地址時,通過某種機制會把虛擬地址轉換成實際物理地址。


這個過程一般通過由操作系統維護的"[頁表][4]"實現,硬件通過"遍歷頁表"進行地址轉換。雖然以頁面爲單位進行地址轉換更容易,但由於每次訪問內存都會發生地址轉換會帶來不小開銷。爲此,引入 [TLB(轉換查找緩衝)][5]緩存最近的轉換記錄。TLB 要求至少要與 L1 緩存一樣快,因此通常緩存少於100條。對工作負載較大的情況,TLB 缺失和由此引發的頁表遍歷需要很多時間。


[4]:https://en.wikipedia.org/wiki/Page_table

[5]:https://en.wikipedia.org/wiki/Translation_lookaside_buffer


雖然不能創建更大的 TLB,但我們還可以做一些別的事情:創建更大的內存頁!大多數硬件都提供4K大小的基本頁,2M/4M/1G"大頁面"。 使用更大的頁面覆蓋同一區域也可以縮小頁表,從而減少頁面遍歷的時間。


在 Linux 世界,至少有兩種截然不同的方法可以在應用程序中做到這一點:


- [hugetlbfs][6]。裁剪一塊系統內存作爲虛擬文件系統,應用程序通過 mmap(2) 對其進行訪問。這是一種特殊接口,需要同時配置操作系統和應用程序後才能使用。這也是一種"全有或全無"的處理:爲 hugetlbfs 分配的(持久化)空間不能爲其它常規進程使用。


- [THP(透明巨型頁)][7]。應用程序可以像平常那樣分配內存,但 THP 會嘗試嚮應用程序透明地提供後臺大頁面存儲支持。理想情況下,啓用 THP 無需修改應用程序,但是我們能夠看到應用程序從中受益。實際上,啓用 THP 會帶來內存開銷或者時間開銷。前者因爲可能會爲一些較小的內容分配整個大頁面,後者因爲 THP 分配頁面有時需要進行內存碎片整理(defrag)。好消息是這裏有一種折衷方法:應用程序調用 madvise(2) 建議 Linux 在何處啓用 THP。


[6]:https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt

[7]:https://www.kernel.org/doc/Documentation/vm/transhuge.txt


我不明白爲什麼術語"large"和"huge"可以互換。 不管怎樣,OpenJDK 支持兩種模式:


```java
$ java -XX:+PrintFlagsFinal 2>&1 | grep Huge
 bool UseHugeTLBFS             = false      {product} {default}
 bool UseTransparentHugePages  = false      {product} {default}
$ java -XX:+PrintFlagsFinal 2>&1 | grep LargePage
 bool UseLargePages            = false   {pd product} {default}
```






`-XX:+UseHugeTLBFS` 把 Java 堆 mmaps 到獨立的 hugetlbfs 中。


`-XX:+UseTransparentHugePages` 用 madvise -s 建議 Java 堆應該使用 THP。這是一個便捷選項,因爲我們知道 Java 堆很大且大部分是連續的,並且極有可能因大頁面受益。


`-XX:+UseLargePages` 是一種啓用所有功能的快捷方式。在 Linux 上,該選項會啓用 hugetlbfs 而不是 THP。我想這是歷史的原因,因爲 hugetlbfs 出現得更早。


一些應用程序在啓用大頁面時確實會[受到影響][8](有時會看到人們爲了避免 GC 手動內存管理,結果卻觸發 THP 碎片整理進而導致延遲達到峯值)。我的直覺是 THP 在生命週期較短的應用程序上效果不佳,這些應用程序碎片整理耗費的時間與應用生命週期相比非常可觀。


[8]:https://bugs.openjdk.java.net/browse/JDK-8024838


 4. 實驗


能否舉例展示大頁面給我們帶來的好處?當然可以。任何一位系統性能工程師在三十多歲時至少運行過一次類似這樣的工作負載,分配並隨機訪問 `byte[]` 數組:


```java
public class ByteArrayTouch {

   @Param(...)
   int size;

   byte[] mem;

   @Setup
   public void setup()
{
       mem = new byte[size];
   }

   @Benchmark
   public byte test()
{
       return mem[ThreadLocalRandom.current().nextInt(size)];
   }
}
```
















(完整源代碼參見[這裏][9])


[9]:https://shipilev.net/jvm/anatomy-quarks/2-transparent-huge-pages/ByteArrayTouch.java


我們知道數組大小各有不同,程序性能可能最終由 L1 緩存失敗、L2 緩存失敗或 L3 緩存失敗決定。這裏通常忽略 TLB 失敗成本。


運行測試前,我們需要確定堆大小。我的電腦 L3 大約8M,所以100M數組足以超過。這意味着用 `-Xmx1G -Xms1G` 分配1G大小的堆就可以滿足測試條件。同時,也可以參照這種方式確定 hugetlbfs 所需資源。


接下來,確保設置下列選項:


```
# HugeTLBFS 應該分配 1000*2M 頁面:
sudo sysctl -w vm.nr_hugepages=1000

# THP 僅進行 "madvise" 建議(一些發行版本提供設置默認值選項):
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
```







我比較喜歡爲 THP 做 "madvise",因爲它允許我選擇已經知道可能受益的特定內存。


在 i7 4790K、Linux x86_64、JDK 8u101 環境下運行:


```java
Benchmark               (size)  Mode  Cnt   Score   Error  Units

# Baseline
ByteArrayTouch.test       1000  avgt   15   8.109 ± 0.018  ns/op
ByteArrayTouch.test      10000  avgt   15   8.086 ± 0.045  ns/op
ByteArrayTouch.test    1000000  avgt   15   9.831 ± 0.139  ns/op
ByteArrayTouch.test   10000000  avgt   15  19.734 ± 0.379  ns/op
ByteArrayTouch.test  100000000  avgt   15  32.538 ± 0.662  ns/op

# -XX:+UseTransparentHugePages
ByteArrayTouch.test       1000  avgt   15   8.104 ± 0.012  ns/op
ByteArrayTouch.test      10000  avgt   15   8.060 ± 0.005  ns/op
ByteArrayTouch.test    1000000  avgt   15   9.193 ± 0.086  ns/op // !
ByteArrayTouch.test   10000000  avgt   15  17.282 ± 0.405  ns/op // !!
ByteArrayTouch.test  100000000  avgt   15  28.698 ± 0.120  ns/op // !!!

# -XX:+UseHugeTLBFS
ByteArrayTouch.test       1000  avgt   15   8.104 ± 0.015  ns/op
ByteArrayTouch.test      10000  avgt   15   8.062 ± 0.011  ns/op
ByteArrayTouch.test    1000000  avgt   15   9.303 ± 0.133  ns/op // !
ByteArrayTouch.test   10000000  avgt   15  17.357 ± 0.217  ns/op // !!
ByteArrayTouch.test  100000000  avgt   15  28.697 ± 0.291  ns/op // !!!
```























下面是一些觀察結果:


  1. 對於較小的數組,緩存和 TLB 表現都很好,與基準測試沒有顯著差別。

  2. 在大數組情況下,緩存失敗開始占主導地位,這就是爲什麼每種配置開銷都在增加。

  3. 對於較大的數組,會出現 TLB 錯誤,啓用更大的頁面非常有幫助!

  4. `UseTHP` 和 `UseHTLBFS` 都能起到幫助,因爲它們嚮應用程序提供了相同的服務。


爲了驗證出現 TLB 失敗這一假設,可以查看硬件計數器。執行 JMH `-prof perfnorm` 會按操作輸出統一結果。


```java
Benchmark                                (size)  Mode  Cnt    Score    Error  Units

# Baseline
ByteArrayTouch.test                   100000000  avgt   15   33.575 ±  2.161  ns/op
ByteArrayTouch.test:cycles            100000000  avgt    3  123.207 ± 73.725   #/op
ByteArrayTouch.test:dTLB-load-misses  100000000  avgt    3    1.017 ±  0.244   #/op  // !!!
ByteArrayTouch.test:dTLB-loads        100000000  avgt    3   17.388 ±  1.195   #/op

# -XX:+UseTransparentHugePages
ByteArrayTouch.test                   100000000  avgt   15   28.730 ±  0.124  ns/op
ByteArrayTouch.test:cycles            100000000  avgt    3  105.249 ±  6.232   #/op
ByteArrayTouch.test:dTLB-load-misses  100000000  avgt    3   ≈ 10⁻³            #/op
ByteArrayTouch.test:dTLB-loads        100000000  avgt    3   17.488 ±  1.278   #/op
```














好了!在基準測試中,每個操作都會發生一次 dTLB 加載失敗,啓用 THP 後會少得多。


當然,啓用 THP 碎片整理後,在分配或訪問時會有碎片整理開銷。爲了將這些成本轉移到 JVM 啓動階段,避免應用程序運行中出現意料之外的延遲問題,可以讓 JVM 在初始化時使用 `-XX:+AlwaysPreTouch` 訪問 Java 堆中的每個頁面。無論如何,爲較大的堆啓用 `pre-touch` 是一個好辦法。


有趣的是: 實際使用中,啓用 `-XX:+UseTransparentHugePages` 讓 `-XX:+AlwaysPreTouch` 變得更快。因爲 JVM 知道,現在它必須以更大的量程(比如每2M一個字節),而不是更小的量程(每4K一個字節)訪問堆。啓用 THP 進程死亡內存釋放速度也會加快,這種粗暴的效果要等到併發內存釋放補丁加入發行版內核纔會結束。


例如,使用 4TB (Terabyte)大小的堆:


```java
$ time java -Xms4T -Xmx4T -XX:-UseTransparentHugePages -XX:+AlwaysPreTouch
real    13m58.167s
user    43m37.519s
sys     1011m25.740s

$ time java -Xms4T -Xmx4T -XX:+UseTransparentHugePages -XX:+AlwaysPreTouch
real    2m14.758s
user    1m56.488s
sys     73m59.046s
```










提交和釋放4TB肯定需要一段相當長的時間了。


5. 觀察


使用大頁面是一種提高應用程序性能的簡單技巧。內核中 THP 讓應用訪問內存變得更加容易。JVM 中對 THP 的支持讓選擇大頁面更方便。當應用程序擁有大量數據和大堆棧時,嘗試使用大頁面總是一個好主意。


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