LWN: 5.13 中對于內存分配進行的兩個優(yōu)化!
關注了就能看到更多這么棒的文章哦~
A pair of memory-allocation improvements in 5.13
By Jonathan Corbet
May 6, 2021
DeepL assisted translation
https://lwn.net/Articles/855226/
在 5.13 合并的許多改動中,也包括對整個內核的性能提升。這種工作通常不如新功能那么顯眼,但它對內核的未來是至關重要的。在內存管理領域,有幾個進行了很長時間的 patch set 終于進入了 mainline。它們提供了 bulk page-allocation interface(批量分配頁面的接口)以及 vmalloc() 進行 huge-page mapping。這兩個改動應該會提升系統(tǒng)的性能,至少是對某些場景來說。
Batch page allocation
內核的內存分配功能長期以來一直在想辦法針對性能和可擴展性(scalability)進行優(yōu)化,但是在有些情況下仍然有改進空間,比如在高速網絡(high-speed networking)場景下。早在 2016 年,網絡相關的開發(fā)人員 Jesper Dangaard Brouer 就介紹了要支持最快的網絡鏈路所面臨的挑戰(zhàn):當系統(tǒng)每秒處理數(shù)千萬個數(shù)據(jù)包時,用來處理任意一個數(shù)據(jù)包的時間就非常有限了。內核可能只有幾百個 CPU 時鐘周期來處理每個數(shù)據(jù)包,而實際上光是從內存分配器(memory allocator)中分配一個 page 本身的時間開銷可能就超過這個限制了。將所有 CPU 時間都花在分配內存上,對獲得最佳性能這個目標肯定是不利的。
在當時的文章中,Brouer 要求提供一個 API,可以在一次調用中就分配許多 page,希望能大幅降低每個 page 的分配成本。然后網絡處理相關的代碼就可以持有一堆內存 page,根據(jù)需要來快速分配 page。當時沒有人反對這個要求。大家都知道,在這樣的情況下,批量操作可以提高吞吐量。但是,這個接口花了不少時間才問世。
Mel Gorman 承擔了這項任務,準備了一系列 patch,走到第六個版本的時候,終于在今年 3 月發(fā)布并被納入 -mm 代碼樹。這組 patch 增加了兩個新的接口,用來分配多個單獨("order-0")頁面,首先介紹這個接口:
unsigned long alloc_pages_bulk(gfp_t gfp, unsigned long nr_pages,
struct list_head *list);
gfp 用來指定進行分配時的 allocation flag,nr_pages 則是調用者希望分配得到的 page 數(shù)量,list 是一個列表用來容納所分配得到的這些 page。返回值是實際分配出的 page 的數(shù)量,畢竟因為一些原因有可能不能滿足 nr_pages 的要求。被分配出來的 page structure 用 list 組織起來(利用了 lru entry),然后添加到 list 參數(shù)指定的 list 中。
這種用鏈表來管理返回的各個 page 的做法可能看起來有點奇怪,尤其是通常來說使用鏈表(linked list)往往不利于得到更好的可擴展性(scalability)。這種做法的優(yōu)點是它不需要分配內存用來跟蹤分配出來的 page。由于這個場景里不太可能會遍歷這個 list(因為不會將這個 list 當作一個整體來關注),所以這里并不會引入可擴展性問題。不過,這個接口對一些人來說可能覺得很笨拙,因此對于這些更希望提供一個數(shù)組來放置這些指針的人來說,可以使用另一個接口:
unsigned long alloc_pages_bulk_array(gfp_t gfp, unsigned long nr_pages,
struct page **page_array);
這個函數(shù)會把分配出來的 page structure 的指針存儲到 page_array 中,當然數(shù)組空間至少要有 nr_pages 這么多,否則可能會出問題。有趣的是,這個接口被實現(xiàn)成只會對 page_array 中的 NULL 項才會分配 page 并添加進去,所以 alloc_pages_bulk_array() 可以用來對一個部分清空的 page 數(shù)組進行補充填充。因此,在第一次調用 alloc_pages_bulk_array()之前,這個數(shù)組必須先被清零。
對于需要進行更多細節(jié)控制的用戶,實現(xiàn) alloc_pages_bulk() 和 alloc_pages_bulk_array() 的底層函數(shù)實際上是:
unsigned int __alloc_pages_bulk(gfp_t gfp, int preferred_nid,
nodemask_t *nodemask, int nr_pages,
struct list_head *page_list,
struct page **page_array);
這里額外多出來的參數(shù)是用來控制從 NUMA 系統(tǒng)中哪個節(jié)點來分配 page。會希望優(yōu)先使用 preferred_nid 這個節(jié)點,而 nodemask(如果有指定這個參數(shù)的話)則用來指定允許 page 分配來自哪些節(jié)點。page_list 和 page_array 中應該有一個不是 NULL,也就會被用來返回分配出的 page。如果兩者都提供了的話,那么將會使用 page_array,也就是忽略 page_list。
這組 patch set 所包含的 benchmark 顯示,在高速網絡場景下的速度提高了近 13%,而 Sun RPC 測試情況下的速度則接近提升了 500%。不過 Gorman 指出:"這個系列的兩個可能的使用場景都是一些不太常見的情況(NFS 和 high-speed networks),所以大多數(shù)用戶短期內可能看不到什么提升。" Sun RPC 和網絡場景相關的改動也已經直接合入了 5.13,后面可能會合入一些其他場景下的相關改動。
Huge-page vmalloc()
內核中大多數(shù)的內存分配函數(shù)都會返回指針,指向 page 或內核的地址空間中的地址。無論是哪種方式,這些指針實際上都對應于被分配出來的內存的物理地址。這對于小范圍的內存分配(比如分配一個 page 或更少)場景來說很好,但是由于內存的碎片化,對于更大分配 size 的物理內存分配就變得越來越難以滿足。由于這個原因,近年來有許多工作都是為了盡可能地避免進行多個頁面的分配(multi-page allocation)。
但有時必須要有一個很大的連續(xù)區(qū)域才行,這就是 vmalloc()接口使用場景了。vmalloc() 分配出的 page 可能分散在物理內存中多個地方,但是會把它們映射到內核地址空間的一個特殊位置,使得它們看起來是連續(xù)的。歷史上由于設置 mapping 的成本比較高,以及 32 位系統(tǒng)上這個專用的地址空間比較小,所以不建議過多使用 vmalloc()。不過,現(xiàn)在 64 位系統(tǒng)上不再有地址空間的限制了,所以隨著時間的推移對 vmalloc()的使用也在不斷增加。
不過,使用 vmalloc() 范圍內的地址的話,比起使用內核直接映射(direct mapping)的地址用起來更慢,因為后者會盡可能地使用 huge page 來進行映射。這就減少了 CPU 的 TLB(translation lookaside table)的壓力。TLB 就是加速虛擬地址解析(不用經過頁表遍歷)的。在 vmalloc() 區(qū)域內的 mapping 都是使用小 page(所謂的"base" page),這對 TLB 來說壓力更大了。
不過從 5.13 開始,vmalloc()可以使用 huge page 來進行一些大范圍的內存分配了,這要感謝 Nicholas Piggin 的 patch。對于超過了 huge-page 的最小 size 的空間進行 vmalloc()分配時,將會嘗試使用 huge page 而不是 base page。正如 Piggin 所描述的,這可以顯著提高某些內核數(shù)據(jù)結構的性能:
內核中幾個最常用的結構(例如 vfs 和 network 的 hash tables)在 NUMA 系統(tǒng)上都是用 vmalloc 分配的,目的是為了在多個機器之間調配訪問的吞吐量。對這些結構使用 huge page 來映射就可以大大改善 TLB 的利用,例如,在一個 2-node POWER9 上的 "git diff" 使用中,可以減少近 30 倍的 TLB miss 次數(shù)(59,800 -> 2,100),并減少 0.54%的 CPU cycle,因為 vfs hash 是用 2MB 的 page 所分配的了。
這里還是會有一些潛在缺點,比如說由于內部碎片而浪費更多內存。例如,分配 3MB 的內存可能會導致占用兩個 2MB 的 huge page,導致有 1MB 內存一直未被使用。當使用 huge page 時,內存在 NUMA 系統(tǒng)中的分布可能不會非常平衡。一些調用 vmalloc() 的函數(shù)可對于返回給它的是個 huge page 這個情況沒有準備,所以不能所有地方都改過去;特別是 module loader,它就在使用 vmalloc()并且有可能改為 huge page 之后會有好處,但目前并沒有改過去。
盡管如此,在目前所做的測試中,為 vmalloc() 使用 huge page 的優(yōu)點看起來還是超過了缺點。并且設計了一個新增的命令行參數(shù),nohugevmalloc=,用來在需要的情況下禁用這種行為。
這些改動對于大多數(shù)用戶來說不太可能會提供非常驚人的性能提升。但是它們是正在進行的盡力優(yōu)化內核行為的工作中的一個重要部分,正是許許多多類似這樣的改動使得 Linux 能有如今這么好的表現(xiàn)。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長按下面二維碼關注,關注 LWN 深度文章以及開源社區(qū)的各種新近言論~
