大頁 struct page 記憶體優化87%+ !HVO 最新優化進展與規劃

語言: CN / TW / HK

歡迎關注【位元組跳動 SYS Tech】公眾號。位元組跳動 SYS Tech 聚焦系統技術領域,與大家分享前沿技術動態、技術創新與實踐、行業技術熱點分析等內容。

大家下午好,今天給大家帶來的主題是《HVO Progress and Plans》 ,即大頁記憶體佔用優化的進度與計劃。

HVO 簡介

Linux 核心一般以 4K 為單位來管理物理頁面。每 4K 實體記憶體對應一個 struct page 結構體,每個 struct page 大約 64 位元組,即 struct page 佔據了 1.56% 的記憶體,那麼每 1T 記憶體會有 16G 的空間用於 struct pages。

Linux 有大頁的功能,每個大頁會有 2M、1G 等不同大小,理論上一個大頁只需要用一個 struct page 來表示,但實際上構成大頁的每個 4K 物理頁在核心中都依然要用一個 struct page 來表示。這些 struct pages 內容相同且用處很少,佔用了大量記憶體,因此我們提出了 HVO 的特性來優化記憶體。

HVO 是 HugeTLB Vmemmap Optimization 的簡稱,可以降低大頁記憶體所對應的 vmemmap 記憶體佔用。其原理是把一個大頁在 vmemmap 中所有 struct page 的虛擬地址都對映到同一個實體地址,以此釋放 struct page 所佔用的實體記憶體。該特性合入到 Kernel 社群之後,也有同學通過實驗發現, HVO 不僅可以降低記憶體的佔用,在 cache 的空間區域性性表現上也更好。因為它將多個虛擬地址對映到了同一塊實體地址,更多 struct page 讀寫操作就會在快取中進行,相應地也提升了 cache 的訪問效率。

當我們開啟 HVO 特性之後,一個 2M 的大頁能夠節省大約 87.5% 的 struct page 記憶體佔用。如果是 1G 的大頁,可以節約的 struct page 記憶體佔用近乎 100%。

  • 以 2M 大頁為例:每個 2M 大頁需要 512 個 struct page 結構體來表示,即總共 512 * 64byte = 32KB 記憶體,佔用 8 個 4KB 的 page,HVO 可以將後面的 7 個 page(28 KB) 全部釋放,並統一對映到第一個 page 的實體地址中,所以達到了節約 87% 的效果。
  • 以 1G 大頁為例:每個 1G 大頁,會有 262144 * 64byte = 16MB 的空間來儲存 struct page。HVO 會將所有的 tail page 都釋放掉,只保留一個 head page 儲存元資訊,因此達到節省近乎 100% 記憶體的效果。

HVO 最新特性

最近一年,我們團隊在 HVO 上又增加了一些新特性:

  • 節省更多記憶體:HVO 在最初合入社群的 patch 版本中,對於一個 2M 大頁只能節省 75% 的記憶體,在今年 3 月份 Free the 2nd vmemmmap page 釋出更新後,可以節省 87% 以上的記憶體,記憶體優化提高了12%。
  • ARM64 適配:HVO 在原有僅支援 x86 架構的基礎上,擴充套件支援了 ARM64 架構。雖然 HVO 程式碼是通用的,但不同架構在開啟 HVO 時仍然需要注意本架構對 struct page 的特異性操作,如刷快取等。
  • Hotplug memmap_on memory 適配:適配了 memmap_on memory,HVO 與 memmap_on memory 在功能性上達到相容。memmap_on memory 可以在熱拔插的記憶體上預留一塊記憶體,以填充這塊記憶體的 struct page。在此之前, HVO 和 memmap_on memory 是互斥的,啟用 memmap_on memory 就不能用 HVO,用了 HVO 就不能用 memmap_on。後來我們發現龍芯社群的程式碼中也開啟了 HVO 特性的優化,非常樂於看到不同的架構適配 HVO,包括 RSIC-V 架構等。
  • 提高被釋放記憶體連續性: 在今年8月份的 patch 上,主要解決了 HVO 釋放記憶體所產生的碎片化問題。
  • 更多場景: 將 HVO 的思想應用在更多場景上,比如  device-dax 等。

如何使用 HVO

Compile HVO

首先確保核心為 5.14 主線之後的版本,如果想用其他的版本,可以 backport 一下。然後要確認啟用配置檔案: CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP = y;重新編譯核心再進入系統,核心啟動日誌會說明當前已開啟哪些型別的大頁,以及 HVO 針對這種大頁會節約多少記憶體。例如下圖中的核心啟動日誌表明:一個 2 M的大頁,HVO 可以節約 28 KB 的 vmemmap 記憶體。

[ 1.047319] HugeTLB: registered 2.00 MiB page size, pre-allocated 0 pages 
[ 1.052204] HugeTLB: 28 KiB vmemmap can be freed for a 2.00 MiB page

需要注意的是:如果打印出下列資訊,則表示當前機器無法啟用 HVO,原因是 struct page 沒有對齊到 64 位元組,以致於不能使用 HVO 將多個 vmemmap 頁對映到同一個實體地址。

Not support: [ 0.933198] HugeTLB: 0 KiB vmemmap can be freed for a 2.00 MiB page

Enable HVO

雖然已經編譯,但系統並不會預設的開啟 HVO 特性,我們可以通過下面三種方法開啟 HVO。開啟 HVO 之後,每一個大頁會節約大約 87% 到 99% 的 struct page 記憶體。

  • 第一種方法:在配置檔案中設定一個預設值,即可預設開啟 HVO ,該方法最為簡單易用。
CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP_DEFAULT_ON=y

  • 第二種方法:在 command line 中傳入 hugetlb_free_vmemmap = on 的引數
hugetlb_free_vmemmap=on

  • 第三種方法: 通過 systemctl 命令動態地開啟或者關閉 HVO 的優化。需要注意的是:當關閉之後,HVO 優化不會立即消失,如果想要徹底關閉 HVO 優化,需要先用 systemctl 關閉 HVO 特性,然後將所有已經申請的大頁都釋放掉,這樣才會在核心徹底地關閉 HVO 特性。
echo 1 > /proc/sys/vm/hugetlb_optimize_vmemmap

Balance space & time

HVO 雖然能夠大幅度節省記憶體,但同時也存在一定效能問題。這是因為 HVO 每次需要從 buddy 裡面申請 hugetlb,然後再對 vmemmap 重新進行地址對映,所以效能稍微差一些。那麼,如何做好空間與時間的動態平衡呢?

echo 1 > /proc/sys/vm/hugetlb_optimize_vmemmap 
echo $RESERVE > /proc/sys/vm/nr_hugepages 
echo 0 > /proc/sys/vm/hugetlb_optimize_vmemmap

echo $OVERCOMMIT > /proc/sys/vm/nr_overcommit_hugepages

解決辦法就是先開啟 HVO 預申請 huge page ,可以達到節約記憶體的目的。然後關閉 HVO 後設置 overcommit。overcommit 的部分雖然不會節約記憶體,但是執行效率較高。也就是前段節省空間,後段提高效能。

支援更多架構

HVO 在原有僅支援 x86 架構的基礎上,擴充套件支援了 ARM64 架構。HVO 將 vmemmap 中的一段虛擬地址都指向一個物理頁面,複用了原來頭部的 struct page,tail struct page 都被回收了。

所以當代碼要操作大頁中的某個小頁,修改 tail page,實際上修改的是 head page 所對應的實體地址,由於 HVO 將 tail page 的虛擬地址空間設定為只讀,一些程式碼可能會報寫錯誤。

如上圖,對 tail page 的虛擬地址發起的讀寫請求,會觸發防寫。

比如 ARM64 中的 flush_dcache_page() 函式,傳一個 struct page 就會設定其標誌位。但如果傳一個大頁中的 tail page 進去,flash 的仍然是整個大頁的記憶體空間。它其實可以做到把所有的 page 都視作一個大頁,只修改 head page 即可,但它卻還是修改了 tail page,所以我們給它做了一個重定向,把它所有對 tail page 的寫操作都重定向到 head page 上去。

使用 HVO 特性的前提條件是:每個 struct page 必須需要 64 位元組或 2 的 N 次冪,否則 struct 的配置就會跨越物理頁的邊界,HVO 無法優化它。未來我們也可以開發一個特性,填充 struct page 到 64 位元組,方便使用者啟用 HVO。這樣做的好處在於:雖然在填充會佔用一些記憶體,但是 HVO 還是在某些場景下節約更多的記憶體。

應用HVO到其他場景上

優化 device-dax

HVO 並不僅僅是一個特性,也是一種思想,可以應用到其他類似的場景中。比如說持久記憶體,當它作為記憶體使用時,需要向用戶提供 device-dax 裝置檔案,以便使用者將其持久記憶體對映到自己的虛擬記憶體空間,對相應的地址範圍進行讀寫操作。device-dax 裝置也需要用 struct page 表示各個頁面。所以後來甲骨文做 device-dax 的同學借鑑了 HVO 的優化思路,提出了針對 device-dax 的優化。

當然 device-dax 只會在載入時進行 HVO 的操作,所以不需要考慮動態分配對效能的影響。

具體可參考:http://lore.kernel.org/all/[email protected]/

優化 buddy

類似的,HVO 的思想也可以應用在 buddy 系統中。比如 buddy 中 order 大於 7 ,由 128+ 個 page 組成的 block,其 struct page 會佔據兩個以上的頁。可以用 HVO 的思路,把 vmemmap 的 tail page 全部對映到 head page 所對應的實體地址,以此來優化記憶體佔用。或者可以新增一個 flag ,從 buddy 申請記憶體時指定我們是否需要這樣優化。

當 buddy 要拆分order 而取消 HVO 優化時,需要用一些 page 將原來釋放的記憶體填充起來,填充記憶體時如果出現記憶體不夠的情況,還要從 buddy 中申請記憶體,如此一來就產生衝突:釋放記憶體的動作需要申請記憶體。

優化透明大頁

再比如透明大頁也可以優化,這是更復雜的場景,因為它們對使用者程序是透明的,可以被隨意分割與合併,這是透明大頁與大頁最大的區別。

當然我們這裡只是提出一種想法,如果有人感興趣的話,可以去嘗試在 buddy 、大頁上進行修改。

HVO 未來計劃

碎片化優化

HVO 碎片化的產生原因:核心採用一種 Sparse Vmemmap 的記憶體管理模式,把記憶體分為一個個的 section,每個 section 是 128 M。HVO 在釋放記憶體時,每隔幾頁會留一個 head page 對映 tail page 的虛擬地址。tail page 會分割空閒空間,導致釋放的頁是碎片化,無法重新組成一個連續的頁面。不利於 buddy 回收記憶體,也不利於 slab 申請 order 比較大的頁。

針對碎片化的問題,我們在 kernel 社群提了一個 patch :在使用者在釋放記憶體時,重新申請一個 page ,把原來的 head page 拷貝進去,以釋放出一塊連續的實體記憶體。如果記憶體足夠大,可以構成一個新的大頁。

相容性優化

在 kernel 社群中還有一個 patch : vmemmap 和 HVO 的相容性,但是做的並不徹底,因為 hot plug 的記憶體拔掉之前,需要把所有分配出去的記憶體都回收回來,包括 HVO 節省出來的記憶體。但這塊還沒有做,感興趣的同學可以去思考一下。