【譯】 JVM Anatomy Park #2: 透明大頁

原文地址:JVM Anatomy Park #2: Transparent Huge Pages

問題

大頁(Large Pages)是什么?透明大頁(Transparent Huge Pages)又是什么?它們有什么用?!

理論

現在虛擬內存被視為理所當然的特性。只有很少人還記得直接操作物理內存的“實模式(real mode)”編程,與此相反,每個進程擁有自己的虛擬地址空間,這段空間將會被映射到實際的物理內存。該特性使得兩個進程可以在相同的虛擬地址0x42424242上具有不同的數據,因為相同的虛擬地址會被映射到不同的物理地址。所以當程序訪問虛擬地址時,某個組件將會把虛擬地址轉換為物理地址。

這部分的功能通常是由操作系統(tǒng)與硬件協同完成的,操作系統(tǒng)負責維護“頁表(page table)”,而硬件通過“頁表移動(page table walk)”轉換地址。以頁的粒度維護地址轉換還是比較容易的,然而這并不高效,因為每次內存訪問都需要做地址轉換!因此這里又對最近的轉換增加了緩存——轉換查找緩存 (TLB)。TLB 通常非常小,少于100條記錄,因為它需要像 L1 緩存一樣快,甚至更快。對于許多工作負載來說,TLB不命中以及相應的頁表移動將會非常耗時。

雖然我們不能把 TLB 做大,但是我們可以把變大!大部分硬件支持4K大小的基本頁,以及2M/4M/1G的“大頁”。擁有更大的頁可以將頁表縮小,使得頁表移動的成本更低。

在 Linux 中至少有兩種方式實現更大的頁:

  • hugetlbfs。占用部分系統(tǒng)內存,將其暴露為虛擬文件系統(tǒng),讓應用程序從其中mmap(2)。這種方式需要操作系統(tǒng)配置和應用程序協同修改。這也是一種“要么全部,要么沒有”的方式:hugetlbfs (持久化部分)分配的空間不能被正常的進程使用。
  • Transparent Huge Pages (THP)。這種方式對應用程序來說是透明的,應用可以像平常那樣分配內存。理想情況下,應用程序不需要做任何改動。但是實際上這種方式存在空間成本(因為對某些小對象也會分配整個大頁)和時間成本(因為有時候THP需要整理內存碎片)。好在存在一個妥協的辦法:應用程序可以通過madvise(2)告訴 Linux 在何處使用 THP。

至于為什么在命名上交替使用了“l(fā)arge”和“huge”,那我就不清楚了。不管怎樣 OpenJDK 兩種方式都支持:

$ 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 堆到 hugetlbfs,

-XX:+UseTransparentHugePages僅僅madvise Java 堆應該使用 THP。這是一個便捷的方式,因為我們知道 Java 堆很大,基本是連續(xù)的,可以最大程度享受到大頁的好處。

-XX:+UseLargePages 是一個通用的配置,用于啟動任意可用的方式。在 Linux 中,該參數啟動 hugetlbfs,而不是 THP。我猜測這可能是歷史原因,畢竟 hugetlbfs 更早出現。

某些應用程序在開啟大頁之后卻造成了性能下降。(很有趣的是人們通過手動內存管理來避免 GC,然而卻由于 THP 的內存碎片整理造成了突增的高延時?。┪业闹庇X是 THP 對大部分生命周期較短的應用程序會造成性能下降,因為內存整理的時間相對于應用程序較短的生命周期比重更明顯。

實驗

我們可以檢驗大頁帶來的好處么?當然,讓我們以一個通常的工作負載為例,分配然后隨機訪問byte[]數組:

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)];
    }
}

(完整的代碼在這里

我們知道隨著數組變大,系統(tǒng)的性能開始受 L1 緩存不命中的影響,然后受L2緩存不命中的影響,然后受L3緩存不命中的影響,以及其他的影響。這里我們通常會忽略 TLB 不命中的影響。

在執(zhí)行測試用例之前,我們需要確定使用多大的堆內存。在我的機器上,L3緩存有 8M,所以 100M 大小的數組就可以超出緩存。這意味著通過-Xmx1G -Xms1G分配1G內存肯定就足夠了。這也為我們分配 hugetlbfs 提供了指導。

所以,確保設置了下述參數:

# HugeTLBFS should allocate 1000*2M pages:
sudo sysctl -w vm.nr_hugepages=1000

# THP to "madvise" only (some distros have an opinion about defaults):
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

我喜歡通過“madvise”使用 THP,因為這使得我可以“選擇性”的使用收益較高的部分內存。

執(zhí)行環(huán)境 i7 4790K, Linux x86_64, JDK 8u101:

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. UseTHPUseHTLBFS兩者的性能改善基本上是一樣的,因為它們提供的功能對應用程序來說是一樣的。

為了驗證 TLB 不命中的猜想,我們可以觀察硬件計數器。JMH的-prof perfnorm提供了操作粒度的數值。

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?3            #/op
ByteArrayTouch.test:dTLB-loads        100000000  avgt    3   17.488 ±  1.278   #/op

讓我們開始分析吧!在基準測試中每次操作都會 dTLB load miss,但是啟動了THP之后卻很少。

當然,伴隨著啟動 THP,你也需要承擔內存碎片整理的成本。我們可以將這個成本轉移到 JVM 啟動時,這樣就可以避免程序運行時突然的卡頓,具體的方法是通過-XX:+AlwaysPreTouch參數控制 JVM 啟動時訪問堆中的每個內存頁。通常來說預訪問是個不錯的選擇。

有趣的事情發(fā)生了:通過設置-XX:+UseTransparentHugePages可以使-XX:+AlwaysPreTouch更快完成,因為 JVM 可以以更大的步長(每2M一個字節(jié))訪問堆,而不是默認情況下較小的步長(每4K一個字節(jié))。進程關閉后釋放內存也會更快,這種優(yōu)勢一直延續(xù)到并行釋放補丁進入發(fā)行版的內核。

使用 4TB 大小的堆內存:

$ 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 的內存確實需要執(zhí)行很長時間!

觀察

大頁是一個改善程序性能的小技巧。Linux 內核中的透明大頁更容易使用。JVM 支持的透明大頁也很容易啟用。通常來說使用大頁是一個好主意,特別是在你的應用程序占用大量數據和堆內存的情況下。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • CPU Cache 今天的CPU比25年前更復雜。那時候,CPU內核的頻率與內存總線的頻率相當。內存訪問只比寄存器...
    blueshadow閱讀 3,216評論 0 5
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,675評論 25 709
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 13,881評論 2 59
  • 又是一年秋招季,哎呀媽呀我被虐的慘來~這不,前幾陣失蹤沒更新博客,其實是我偷偷把時間用在復習課本了(霧 堅持在社區(qū)...
    tengshe789閱讀 2,149評論 0 8
  • 2018-06-03 姓名 :李宏清(單位)揚州市方圓建筑工程有限公司 哈爾濱363期反省二組 【日精進打卡第 ...
    李宏清閱讀 199評論 0 0

友情鏈接更多精彩內容