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

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

問題

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

理論

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

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

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

在 Linux 中至少有兩種方式實現(xiàn)更大的頁:

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

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

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

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

實驗

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

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

(完整的代碼在這里

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

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

所以,確保設(shè)置了下述參數(shù):

# 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,因為這使得我可以“選擇性”的使用收益較高的部分內(nèi)存。

執(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 // !!!

這里觀察到一些現(xiàn)象:

  1. 當(dāng)數(shù)組比較小時,緩存和TLB都合適,那么性能相對于基準(zhǔn)線沒有差異。
  2. 隨著數(shù)組增大,緩存不命中成為影響性能的主要因素,所以在三種場景下耗時都增加了。
  3. 隨著數(shù)組增加,TLB不命中也產(chǎn)生了影響,所以通過設(shè)置大頁改善了一些!
  4. UseTHPUseHTLBFS兩者的性能改善基本上是一樣的,因為它們提供的功能對應(yīng)用程序來說是一樣的。

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

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

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

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

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

使用 4TB 大小的堆內(nèi)存:

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

觀察

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

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

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容