RFC-0219:Zircon Page 壓縮

RFC-0219:Zircon 頁面壓縮
狀態已接受
區域
  • 核心
說明

在匿名 VMOs 的頁面中進行核心壓縮。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-05-31
審查日期 (年-月-日)2023-06-03

摘要

本文建議為匿名使用者記憶體新增核心內壓縮和解壓縮系統。

雖然系統位於核心中,但由於記憶體計算的影響,系統並未完全對使用者空間公開。不過,除了 API 異動之外,所有功能異動都會納入核心。

提振精神

Fuchsia 經常用於資源受限的裝置,因為在這些裝置上,增加記憶體集區可大幅改善使用者體驗。

其他作業系統使用的標準技巧就是記憶體的核心壓縮,支援這項技巧有助於 Fuchsia 達到一致性。

相關人員

協助人員:

cpu@google.com

審查者:

eieio@google.com、rasheqbal@google.com、maniscalco@google.com

諮詢:

mseaborn@google.com、mvanotti@google.com、wez@google.com、plabatut@google.com、jamesr@google.com、davemoore@google.com

社會化:

核心設計和實作部分的 RFC 前提案文件,已分享給 Zircon 核心團隊和其他利益相關者。

設計

本節將概略說明實作、壓縮策略和建議的 API 變更和擴充功能的高層級設計選項。

老化和追蹤

為了計算頁面存活時間,匿名頁面應與分頁器支援的頁面採取相同處理方式,這表示:

  • 在匿名頁面中新增回連結,以便執行 vm_page_tVmObjectPaged + 偏移值的查詢。這會增加成本,因為每次網頁在 VMOs 之間移動時,都需要更新回連結。
  • 變更頁面佇列,以便不對分頁器支援和匿名頁面採取不同的處理方式。

頁面佇列中的老化功能已具備有效和無效集合的概念。現有的淘汰作業永遠不會從活動集淘汰項目,以免發生資源耗盡的情況。匿名頁面會屬於相同的佇列,並具有相同的壓縮限制。

如果網頁已壓縮,系統會將其從網頁佇列追蹤中移除,類似於遭到淘汰的網頁。無法壓縮的頁面會移至另一個清單,以免再次嘗試壓縮。如果有人存取這類頁面,系統會將該頁面移回至有效集,並嘗試再次壓縮。

雖然我們只會壓縮閒置的網頁,但我們支援三種觸發事件:

  • 處理 LRU 佇列時,即使未處於記憶體壓力下,也會積極壓縮舊頁面。
  • 在記憶體壓力下進行壓縮,並執行淘汰作業。
  • 壓縮功能可避免發生 OOM。

這些觸發事件會透過核心 cmdline 旗標切換。

VmPageList 標記

VmPageList 商店需要額外提供一種標記,可用於:

  • 表示網頁已存在,但已壓縮。
  • 儲存足夠的中繼資料,以便在壓縮儲存空間中找到網頁。

VmPageList 的項目已經過編碼,可用於儲存 vm_page_t 指標和零頁標記,並可擴充以便進行額外編碼。

目前網頁清單項目是 64 位元值,會編碼為下列選項:

  • 0:空白項目
  • 最低有效位元 = 1:零頁標記
  • 其他:指向 vm_page_t

這項配置方案可運作,是因為指標對齊會使 vm_page_t 指標的底部 3 位元一律為零。因此,我們可以將這個配置法則一般化,讓網頁清單項目的底部 3 位元代表類型,其餘位元則是相關聯資料。初始類型如下:

  • 0:空白項目或 vm_page_t 指標 (視是否設定資料位元而定)
  • 1:零頁標記。剩餘的資料位元必須為 0。
  • 2:經過壓縮的網頁。剩餘位元是從壓縮系統傳回的不透明權杖。
  • 3-7:目前無效的類型。

壓縮作業

為了專注於簡單且有效率的初始用途,初始儲存空間策略會執行單頁壓縮和解壓縮作業,且不會依據 VMO 明確分組網頁。

不按 VMO 分組頁面,可讓壓縮路徑以與現有淘汰系統相同的方式提供。雖然不同 VMOs 的頁面會在單一儲存空間系統中混雜,但嚴格規定不得在不同 VMOs 之間建立鎖定依附元件。這麼做可防止透過時間管道洩漏資訊,並避免對延遲時間過長的無關程序處以懲罰。

為避免不必要的鎖定爭用情形,執行壓縮或解壓縮演算法時,請勿持有 VMO 鎖定,且在持有解壓縮所需的任何鎖定時,請勿執行壓縮。

壓縮演算法

以下概略說明壓縮路徑,並省略錯誤處理案例。壓縮的輸入內容是網頁、網頁所屬的 VMO,以及該網頁在 VMO 中的偏移量。偏移量用於驗證在取得 VMO 鎖定後,頁面是否仍在 VMO 中,這屬於錯誤處理,否則會在這個擬似程式碼中略過。

Take VMO lock
Update page list to use temporary compression reference
Release VMO lock
Compress page to buffer
Take compressed storage lock
Allocate storage slot
Release storage lock
Copy buffer to storage
Take VMO lock
Check page list still holds temporary compression reference
Update page list to final compression reference
Release VMO lock

一開始使用暫時參照而非立即分配參照的原因,是為了讓壓縮儲存系統在產生參照時,提供將要儲存的資料大小,進而提供更大的彈性。這樣一來,系統就能代表參照資料的確切位置,而不需要透過額外表格間接處理。

即使未使用臨時參照,仍需要兩次取得 VMO 鎖定,才能解決在壓縮期間使用頁面的任何潛在競爭問題。舉例來說,在壓縮期間,網頁 (在本例中為參照) 可能已取消提交,除非我們再次取得 VMO 鎖定並檢查,否則我們不會知道這點。

在壓縮期間,由於 VMO 鎖定未保留,因此其他執行緒可能會找到暫時參照並嘗試使用。由於暫時參照資料實際上沒有可解壓縮的資料,因此我們有兩種方法可解決這個問題:

  • 等待壓縮完成,然後擷取原始網頁。
  • 複製原始網頁 (壓縮執行作業開始時會預先分配記憶體)。

這麼做的原因是為了確保網頁在整個壓縮時間內處於已擁有且已知的狀態。在壓縮期間使用原始網頁會造成以下問題:

  • 在壓縮演算法讀取網頁時修改網頁,會增加壓縮演算法的潛在錯誤和攻擊面,因為壓縮演算法可能無法承受並行資料變更。
  • VMO 可能會變更其快取功能或其他屬性,導致壓縮器的並行快取用途不明確。

建議您複製原始網頁,因為這應該是極不可能發生的情況,而且複製作業可更快解決要求,並避免需要使用其他同步處理機制。這裡的複製作業是在 VMO 鎖定下進行,但這與其他所有寫入時複製的路徑一致,在這些路徑中,頁面複製作業可以在 VMO 鎖定下進行。由於一次只會進行單一壓縮,任何 VMO 作業最多只能觸發一次此情況。

解壓縮演算法

我們實作瞭解壓功能,以便運用現有的 PageRequest 系統,該系統已用於延遲分配作業,並滿足使用者的 pager 要求。就像使用者 pager VMO 的缺少頁面會產生頁面要求一樣,VMO 頁面清單中的壓縮參照也會觸發類似的程序:

Take VMO lock
Observe page is compressed
Fill in the PageRequest
Drop VMO lock
Wait on the PageRequest
Retry operation

填入網頁要求時,我們需要從網頁清單中儲存壓縮的參照資料,以及 VMO 和偏移量。這需要擴充現有的 PageRequest 結構。

與使用者分頁器頁面要求和壓縮的差異在於,等待動作可以直接解決要求,也就是在本例中執行解壓縮作業。

解壓縮作業必須容許多個執行緒嘗試存取相同的頁面,並提供優先順序繼承流程:

Declare stack allocated OwnedWaitQueue
Take compressed storage lock
Validate compressed reference
if compressed page has wait queue then
    take thread_lock
    release compressed storage lock
    wait on referenced wait queue
else
    store reference to our allocated wait queue in compressed block
    release compressed storage lock
    decompress into new page
    take VMO lock
    update VMO book keeping
    release VMO lock
    take compressed storage lock
    take thread lock
    remove wait queue reference
    release compressed storage lock
    wake all threads on wait queue

這裡使用 thread_lock 的原因,是因為執行等待佇列作業時需要保留鎖定,且沒有其他屬性可用於保留 thread_lock

目標壓縮大小

由於解壓縮作業會產生相關費用,因此只有在網頁大小縮減幅度足夠時,才值得儲存壓縮版本。否則,系統可能會儲存未壓縮的版本,節省解壓縮費用。

這個目標大小應為可調整的選項,但在其他系統中,70% 的目標相當常見。

系統會將任何無法符合這個目標的壓縮嘗試視為失敗,並將該頁面放回頁面佇列中,如「老化和追蹤」一節所述。

零頁面掃描

由於壓縮功能基本上需要檢查來源頁面中的所有位元組,因此可用於偵測和移除零頁面。這代表壓縮的替代成功結果,其中會回報輸入頁面只是零頁面,且不會將結果儲存在壓縮儲存系統中。相反地,VMO 呼叫端可以使用零頁面標記取代該頁面。

現成的壓縮演算法實作通常沒有方法可回報輸入內容全為零,因此要實現這項功能,您必須採取下列做法之一:

  • 修改或建立導入程式,以便執行這項追蹤。
  • 將壓縮結果與已知簽名進行比較。

第二個選項則是利用壓縮演算法是確定性的事實,且通常在十幾個位元組內有零個網頁表示法。因此,執行大小檢查,接著進行記憶體比較檢查,可能會相當便宜。

這項提案不需要初始壓縮實作來執行零頁偵測,但必須建構 API 和 VMO 邏輯來支援這項功能。最終,現有的零頁掃描器應會替換,以便由壓縮功能支援。

壓縮演算法

我們建議使用 LZ4 做為初始壓縮演算法。詳情請參閱「實驗評估」一節,但其中有以下重要規定:

  • 對於小型檔案 (4 KiB) 來說,這種方法相當實用。
  • 在穩定的初始化後作業期間,不會執行 malloc 或等效作業。
  • 可在沒有共用可變狀態的情況下並行解壓縮。壓縮作業必須能夠與解壓縮作業並行。
  • 雖然不強制規定,但系統本身支援多重並行壓縮。

LZ4 還有其他實用的屬性,可根據受限的目標緩衝區大小提早中止壓縮。

壓縮儲存空間

壓縮頁面的儲存空間是程式碼複雜度和執行時間之間的權衡,可能會導致分散和增加記憶體使用量。選擇初始策略,將固定數量的壓縮頁面 (在本例中為三個) 塞入單一儲存頁面。

這項儲存策略的核心概念是,每個儲存頁面可視為具有左、中和右儲存插槽,可提供 3:1 的最佳壓縮比。

實作方式相當簡單,包括:

  • 搜尋任何現有的部分填入的網頁,找出空缺的時間段落。
  • 如果沒有合適的現有版位,請將廣告活動放在新頁面的左側版位。

這兩個小工具分別是:

  • 部分填入的網頁應根據空白區塊的大小,分類至搜尋清單,以便透過 O(1) 搜尋進行近乎最佳的填入。
  • 如果左側或右側的資料槽含有大量資料,中間的資料槽可能會無法使用。

這種策略的好處如下:

  • 導入方式簡單。
  • 可預測的作業,且沒有長尾效能。
  • 在最壞的情況下,不會使用任何額外記憶體。

可能需要處理的一種異常行為是,一旦頁面經過解壓縮,可能就會出現可填補的空洞,並讓其他儲存頁面釋放。在需要時,可以實作這類的壓縮系統。

長期來說,這可能會演變成使用或成為更通用的堆積或區塊樣式配置工具。

如需初步結果,請參閱「實驗評估」一節。

會計和指標

雖然壓縮作業不會受到使用者輸入內容的影響,但會影響記憶體用量回報方式。我們也想將壓縮效能相關的其他資訊洩漏到使用者空間。

內容與已提交

現有的查詢介面會將已提交的位元組和頁面視為直接在 VMO 中分配用於儲存資料的實際記憶體。

這些現有查詢如下:

  • mem_private_bytesmem_shared_bytes 等中回報已用記憶體的 ZX_INFO_TASK_STATS
  • ZX_INFO_PROCESS_MAPS:回報已對應 VMO 的 committed_pages
  • 回報 committed_bytesZX_INFO_PROCESS_VMOS / ZX_INFO_VMO
  • ZX_INFO_KMEM_STATS / ZX_INFO_KMEM_STATS_EXTENDED 會回報 vmo_bytes,並計為已提交。

除了已提交的內容外,我們還會新增內容位元組/頁面的額外概念。任何談論已提交記憶體的查詢,都會繼續套用至直接在 VMO 中保留內容的記憶體。內容則是指核心為使用者保留的資料量,但不會具體說明如何保留。也就是說,內容可以直接保留在網頁中,因此也會計入已提交的內容;或是經過壓縮,因此不會計入已提交的內容。

在這個規模下,系統會將經過壓縮的內容視為未知的儲存空間大小。嘗試在儲存空間中歸因於個別壓縮頁面會變得複雜且不穩定,因為儲存空間大小取決於整體系統壓縮行為和由此產生的碎片化。

雖然匿名 VMOs 一開始會設為理論上的零,但這並非核心記得的內容。如果使用者明確提交或修改任何網頁,該網頁就會成為內容。此後,即使使用者離開或將內容重設為零,核心也不必注意這點,可能會繼續將其視為追蹤的內容。

在與淘汰和使用者分頁器相關的情況下,可從使用者分頁器擷取的內容不會視為核心追蹤的內容。因此,驅逐會從已承諾和內容金額中扣除。

指標

為了瞭解壓縮效能 (包括調校 / 開發,以及持續監控系統健康狀態),我們希望收集並提供指標。

這些指標應可回答有關壓縮功能效能以及對系統效能影響的相關問題。具體來說,我們想瞭解以下資訊:

  • 壓縮儲存比率。
  • 網頁壓縮的時間長度。
  • 哪些類型的網頁會進行壓縮,以及觸發壓縮的因素。
  • 解壓縮延遲時間。
  • 執行壓縮和解壓縮作業所需的 CPU 時間。
  • 成功和失敗的壓縮嘗試次數比率。

API 變更

透過結構體演進和查詢版本管理功能進行擴充:

  • zx_info_maps_mapping_t 具有 content_pages 欄位。
  • zx_info_vmo_t 具有 content_bytes 欄位。
  • ZX_INFO_TASK_RUNTIMEpage_fault_decompress_time 回報為總 page_fault 時間的子集。

需要為這些項目建立版本的 zx_object_get_info 查詢如下:

  • ZX_INFO_PROCESS_MAPS
  • ZX_INFO_PROCESS_VMOS
  • ZX_INFO_VMO
  • ZX_INFO_TASK_RUNTIME

zx_info_vmo_tcommitted_bytes 等現有欄位的註解和說明文件會更新,以便清楚說明這些欄位不會計算處於壓縮狀態的網頁。

具體內容尚未確定,但建議在報表中新增 ZX_INFO_KMEM_STATS_COMPRESSION 查詢:

struct zx_info_kmem_stats_compression {
    // Size in bytes of the content that is current being compressed.
    uint64_t uncompressed_content_bytes;

    // Size in bytes of all memory, including metadata, fragmentation and other
    // overheads, of the compressed memory area. Note that due to base book
    // keeping overhead this could be non-zero, even when
    // |uncompressed_content_bytes| is zero.
    uint64_t compressed_storage_bytes;

    // Size in bytes of any fragmentation in the compressed memory area.
    uint64_t compressed_fragmentation_bytes;

    // Total amount of time compression has spent on a CPU across all threads.
    // Compression may happen in parallel and so this can increase faster than
    // wall clock time.
    zx_duration_t compression_time;

    // Total amount of time decompression has spent on a CPU across all threads.
    // Decompression may happen in parallel and so this can increase faster than
    // wall clock time.
    zx_duration_t decompression_time;

    // Total number of times compression has been done on a page, regardless of
    // whether the compressed result was ultimately retained.
    uint64_t total_page_compression_attempts;

    // How many of the total compression attempts were considered failed and
    // were not stored. An example reason for failure would be a page not being
    // compressed sufficiently to be considered worth storing.
    uint64_t failed_page_compression_attempts;

    // Number of times pages have been decompressed.
    uint64_t total_page_decompressions;

    // Number of times a page was removed from storage without needing to be
    // decompressed. An example that would cause this is a VMO being destroyed.
    uint64_t compressed_page_evictions;

    // How many pages compressed due to the page being inactive, but without
    // there being memory pressure.
    uint64_t eager_page_compressions;

    // How many pages compressed due to general memory pressure.
    uint64_t memory_pressure_page_compressions;

    // How many pages compressed due to attempting to avoid OOM or near OOM
    // scenarios.
    uint64_t critical_memory_page_compressions;

    // The nanoseconds in the base unit of time for
    // |pages_decompressed_within_log_time|.
    uint64_t pages_decompressed_unit_ns;

    // How long pages spent compressed before being decompressed, grouped in log
    // buckets. Pages that got evicted, and hence were not decompressed, are not
    // counted here. Buckets are in |pages_decompressed_unit_ns| and round up
    // such that:
    // 0: Pages decompressed in <1 unit
    // 1: Pages decompressed between 1 and 2 units
    // 2: Pages decompressed between 2 and 4 units
    // ...
    // 7: Pages decompressed between 64 and 128 units
    // How many pages are held compressed for longer than 128 units can be
    // inferred by subtracting from |total_page_decompressions|.
    uint64_t pages_decompressed_within_log_time[8];
};

如要回報網頁壓縮所需的時間,就必須為每個壓縮的網頁記錄時間戳記。這應該不會對潛在的壓縮比產生重大影響,因為這樣做可讓您瞭解網頁是否正在快速解壓縮。

ZX_INFO_KMEM_STATS_EXTENDED 頁面管理器位元組

ZX_INFO_KMEM_STATS_EXTENDED 查詢的欄位與 pager 支援的 VMOs 中位元組數量有關。這些欄位具體如下:

    // The amount of memory committed to pager-backed VMOs.
    uint64_t vmo_pager_total_bytes;

    // The amount of memory committed to pager-backed VMOs, that has been most
    // recently accessed, and would not be eligible for eviction by the kernel
    // under memory pressure.
    uint64_t vmo_pager_newest_bytes;

    // The amount of memory committed to pager-backed VMOs, that has been least
    // recently accessed, and would be the first to be evicted by the kernel
    // under memory pressure.
    uint64_t vmo_pager_oldest_bytes;

建議的實作方式會在頁面佇列中統一使用分頁器支援和匿名的 VMO 頁面,因此提供這類資訊將不切實際。因此,這項提案將重新定義這些用語,以代表所有可分頁 / 可回收的記憶體,而非僅限於特定可驅逐的使用者分頁器所支援的 VMOs。否則,系統會保留總數、最新和最舊的定義。

雖然壓縮記憶體的回收作業並不會直接增加 PMM 可用記憶體,但這些查詢並非用來代表可回收的確切 PMM 記憶體數量。而是提供各世代記憶體相對分布的洞察資料,並可用於執行驗證,例如 oldest_bytes 會下降,並在裝置 OOM 時接近零等。

為使用者分頁器支援的可剔除記憶體和可壓縮匿名記憶體提供多個欄位,也值得懷疑。在某些情況下,區分這兩者會很有幫助,但大多數情況下,我認為這兩者匯總報表才是您想要的。

停用壓縮 / 延遲時間敏感的 VMOs

對於延遲時間敏感的應用程式,需要有一種機制可在 VMOs 上停用壓縮功能,因為無論解壓縮延遲時間多短,都可能超出應用程式可容忍的程度。

避免回收和其他可能增加記憶體存取延遲的核心活動,是我們正在解決的現有問題。因此,我們不會在此提出任何設計,但這項工作是壓縮功能的依附元件。

擴充功能

除了這個最初提出的設計之外,我們還可以進行一些探索和改善。雖然這些通常屬於實作細節,但我們還是將這些內容納入本文,以便推動這項設計的長期適用性。以下是絕對必須進行的改善項目 (最好是在產品上使用前完成):

  • 移除個別的零頁重複內容,改用壓縮功能。

您也可以嘗試其他可能帶來實用效益的選用做法:

  • 壓縮分頁器支援的記憶體,再考慮是否要將其逐出。
  • 支援在記憶體壓力出現前,執行壓縮作業,但不捨棄未壓縮的頁面。
  • 檢查其他舊版網頁清單,並及早壓縮網頁,而非僅在 LRU 佇列中壓縮。

您也可以進一步調整壓縮儲存空間和壓縮演算法,以便直接進行演進,或為不同產品提供選項。視侵入性或差異程度而定,這些變更可能需要個別的 RFC。

實作

導入程序會分成多個階段,如下所述。

常見的鷹架

我們會先實作常見的 VM 系統變更,以便支援匿名頁面的追蹤和淘汰。這些變更不會影響行為或 API,但風險最高,因此先進行這些變更可讓您:

  • 樹狀圖中已進行變更,並進行了最長時間的測試。
  • 能夠評估候選壓縮資源池 (即舊的匿名頁面)。

核心實作

完成常見變更後,主要核心實作項目即可在功能旗標後的樹狀結構中落地。這樣一來,您就可以先測試壓縮功能的導入作業,確保其穩定性,再透過任何 API 公開其存在。

API 演進

針對指標和計算變更,執行 API 演進和結構遷移作業。由於核心實作項目位於樹狀結構中,因此 API 變更可以連結,但壓縮功能仍會隱藏在功能旗標後方,因此除了額外欄位之外,一般使用者不會看到任何功能變更。

整合、擴充功能和調整

導入作業經過測試且能夠回報指標後,您就可以在不同產品上進行評估,找出任何問題,並執行擴充和調整作業。

這項測試的結果會決定以下事項:

  • 壓縮功能預設為開啟,部分產品會選擇不啟用,或
  • 壓縮功能預設為關閉,部分產品會選擇啟用。

擴充功能和調整

我們現在可以推出部分擴充設計,並持續調整一般實作方式。

成效

我們需要評估成效的兩個面向。

常見的額外負擔

即使已停用壓縮功能,仍需要對常見的 VM 程式碼進行重大變更,這些變更會影響效能。

預期在匿名頁面中加入反向連結,會為所有 VMO 複本建立和刪除事件增加可測量的額外負擔。這些作業並非在效能關鍵路徑上,因為它們通常會在初始化和拆解步驟期間發生,但仍應檢查對產品的任何影響。

其他 VMO 路徑會產生一些維持回連和更新頁面的額外負擔,但這應該是噪音,不會對成效產生可測量影響。

這些路徑涵蓋現有的 VMO 微型基準測試,這將是主要驗證工具。

壓縮效能

壓縮功能的核心是 CPU 與記憶體的權衡,因此只能在產品及其需求的情況下評估其效能。

否則,建議的指標 API 可讓您瞭解壓縮的成本,然後使用壓縮可調整項目控制這些成本,以便節省記憶體。

安全性考量

時間管道

雖然壓縮的網頁可能會有共置儲存空間,但存取這類儲存空間的程序已定義,因此不會在 VMOs 上產生可導向可評估管道的傳遞依附元件。這項設計是為了避免類似記憶體重複使用攻擊的攻擊。

即使沒有傳遞式依附元件,如果攻擊者可以控制與機密資料在同一頁面上同時顯示的資料,仍有可能建立時間管道。這類資料可以設計成根據機密資料來決定壓縮是否成功或失敗,讓攻擊者瞭解一些關於機密資料的資訊。這類攻擊是否實際可行尚未明朗,但如果您擔心這類問題,也可以使用相同的機制,避免對延遲敏感的工作進行壓縮。

系統行為管道

您可以直接透過 ZX_INFO_KMEM_STATS_COMPRESSION 查詢整體系統記憶體用量,也可以查詢 VMO 等物件,並觀察內容和已提交位元組之間是否有差異,藉此推斷整體系統記憶體用量。您也可以查詢 ZX_INFO_KMEM_STATS_COMPRESSION 中的其他統計資料。

這項資訊會間接提供整體系統行為的相關資訊。不過,我們認為,即使您知道自己控制的 VMO 中的網頁是否已壓縮,也無法從中瞭解其他程序的具體內容。

LZ4

雖然 LZ4 程式庫的檔案數和 LoC 指標很少,但仍是使用低階 C 風格編寫的非瑣碎程式碼,可能會有錯誤。程式庫需要以下組合:

  • 安全性審查,確保整體合適性。
  • 額外的強化和測試。
  • 可能需要重寫有問題的部分。

結合強化和重寫功能,可將部分或全部實作項目移植至更安全的表示法,例如 Rust 或 WUFFS。

測試

與成效類似,測試也有兩個維度。

VM 正確性

我們會透過單元測試和整合測試,評估常見的 VM 變更和壓縮相關變更的技術正確性。

對於一般 VM 變更,無論是否在核心單元測試中啟用壓縮功能,都會產生影響,因此我們會視情況新增額外的核心測試。

使用 QEMU 執行工具的整合測試將用於啟用壓縮功能來執行其他測試。

系統行為

雖然壓縮功能並非免費,但不應對產品擁有者認為重要的行為造成負面影響。這些行為會因產品而異,但舉例來說,在記憶體不足的情況下,音訊會跳過,或是導致資源耗盡情形惡化,並導致使用者體驗不佳。

這包括手動測試產品、使用鎖定競爭追蹤等工具,以及與產品測試團隊和產品擁有者合作進行評估。

說明文件

我們會記錄 zx_object_get_info 查詢的新增項目和變更,以及其他 cmdline 選項。

缺點、替代方案和未知事項

未知數和疊代

只有在正確實作並在實際產品上評估後,才能完全瞭解這項提案的確切效益和成本。雖然這項提案會盡力為所有演算法和結構提供具體且合理的初始提案,但這些提案幾乎肯定會隨著實際使用資料而變更。

缺點

這項提案的主要缺點,是會在核心的 VM 系統中增加複雜度。雖然只要停用壓縮功能,就能避免任何壓縮效能問題,但壓縮功能的存在需要變更一般基礎架構。因此,即使未使用壓縮功能,也存在永久的複雜性和風險。

使用者分頁器壓縮替代方案

這項作業不必在核心中執行壓縮和解壓縮,而是可透過使用者分頁機制外包至使用者空間。在許多方面,這會類似於 Linux 上的 ZRAM,因為 ZRAM 是由使用者空間檔案系統驅動程式庫實作。但不幸的是,它也存在許多相同的缺點。

優點

在使用者空間中進行壓縮有兩個主要原因:

  1. 從核心中移除複雜的壓縮和解壓縮程式碼。
  2. 提供靈活的使用者導入方式。

很遺憾,(1) 並非完全正確,因為除非為每個安全性網域建立個別的密封例項 (但這會如何決定),否則這個使用者空間程序就能查看系統中每個程序的匿名記憶體。這會讓系統變得非常可信,因為任何人都可以查看及編輯系統中的任何使用者記憶體。因此,相較於核心,信任此處壓縮和解壓縮程式碼的門檻不會降低。

提供靈活的實作方式絕對值得,但如果有一種機制可將特定匿名 VMO 指派給特定壓縮器,就會更有說服力。這時可能甚至不需要進行壓縮,但可以實作交換或任何其他策略。

缺點

使用者空間壓縮的主要缺點,是它對核心提供的限制。能夠以樂觀主義的方式壓縮及解壓縮網頁 (甚至可能不丟棄未壓縮的網頁),可讓核心提供極佳的實作彈性。所有這類配置方案都可以透過使用者分頁器實作,但這會變成更複雜的分散式系統問題,需要非常謹慎的 API 和並行推理。

雖然 Zircon 是元件系統,且旨在降低切換使用者空間工作負載的成本,但需要向使用者空間程序下行呼叫,才能解壓縮 4 KiB 頁面,這會大幅增加延遲和成本。這麼做可鼓勵延後壓縮,直到頁面過舊為止,進而減少記憶體的潛在節省量。針對現有使用者尋呼服務錯誤,系統會假設正在查詢緩慢的永久性儲存空間,因此已盡可能延後淘汰作業。

即使使用者空間會處理壓縮和解壓縮作業,仍必須執行所有核心變更,才能追蹤網頁年齡並觸發壓縮作業。因此,所有可調整項目和指標仍需存在於核心中。此外,VM 系統目前無法將使用者分頁器與子 VMOs 建立關聯,因此需要進一步變更及重新設計 VM 系統。

核心程式碼解譯器

我們可以透過使用者提供的核心程式碼,支援使用者提供的演算法,而非固定的壓縮演算法,就像 Linux eBPF 一樣。這樣一來,使用者就能靈活實作,不會因為使用者下行呼叫而影響效能。

雖然不需要明確的模式轉換,但核心仍需將此視為類似於使用者下行呼叫,因為效能特徵會不明,因此在叫用提供的程式碼時,仍需小心鎖定和其他依附元件。

這種做法的主要缺點是,如果核心中的 VM 具備足夠的效能,可產生可接受的壓縮效能,就需要實作大量的實作項目,而這項作業必定相當複雜,並大幅增加核心的受信任運算基礎。

未來

日後,我們可能會希望以某種方式讓匿名 VMOs 使用 Swap 儲存空間。這項功能必須透過使用者分頁機制,在使用者空間中實作。此時,這種交換實作可以進行壓縮,而非只交換。不過,由於深層核心整合壓縮功能的優點,我認為這項功能應與核心壓縮功能共存,而非取代它。

會計替代方案

目前的提案建議定義新的內容位元組/頁面,以搭配現有的已提交計數。您可以不變更計數項目,改用其他命名選項,例如:

  • 居民和承諾
  • 居民和內容

每個變更都需要變更已提交的所有現有用途,是更大規模的變更,並在兩個 API 版本之間造成更大的斷層。

ZX_INFO_KMEM_STATS_EXTENDED 分頁器位元組替代方案

雖然建議的實作方式會將分頁器支援和匿名佇列統一,因此無法輕易分離計數,但其他實作方式可以保留計數。

這個替代實作方式會為每個可回收佇列提供兩組不同的計數器。在 vm_page_t 中儲存的 page_queue_priv 中,高位元會用於在將頁面移至佇列之間時,區分需要更新的計數。

這種做法的缺點如下:

  • page_queue_priv 的使用方式變得更複雜。它現在會成為已壓縮的欄位,而非計數器,所有讀取和寫入作業都需要保留或遮蔽額外的位元。
  • 當 arch aspace 鎖定時,系統會執行存取收集作業,由於需要解碼佇列資訊並更新正確的計數,因此成本會增加。
  • 多個計數欄位,且維持一致性更為複雜。

如提案所述,考量到實作複雜度和執行階段成本的增加,我認為針對 pager 位元組欄位實際用途,分別計算計數的價值不大。

實驗評估

為了提出 LZ4 壓縮演算法和壓縮儲存策略,我們建立了實驗性壓縮版本,並納入足夠的組件,以便:

  • 收集要壓縮的無效匿名網頁的樣本資料集。
  • 對這個資料集執行不同的壓縮演算法。

這項測試的目的是評估不同壓縮演算法,包括:

  • 來自實際產品的實際輸入資料。
  • 在與核心相符的受限執行環境中。

第二點特別重要,因為核心程式碼在執行時不支援 FPU/SSE/AVX 等。

我們使用 64 MiB 輸入資料集執行評估,並將每個 4 KiB 頁面個別饋送至壓縮器,以便進行實際操作。系統會以以下方式解讀產生的資料:

  • 成功壓縮是指壓縮後的大小至少為原始大小的 70%。
  • 系統會為所有網頁計算壓縮時間,包括最終未儲存為壓縮版本的網頁。這與實際使用情況相符,因為在實際使用情況下,我們無法事先得知壓縮是否成功。
  • 只有成功壓縮的網頁才會計入解壓縮時間。
  • 總大小會計算每個網頁所需的儲存空間,因此成功壓縮的網頁會將壓縮大小加入計數,而壓縮失敗的網頁則會計為 4 KiB。

這種計數方式的理由在於,最終目標是釋放記憶體並使用最少的 CPU。壓縮演算法有時會快速且有效地壓縮網頁,但有時會耗用大量 CPU 而無法壓縮,因此實際上既無法節省太多記憶體,又會使用大量 CPU。

NUC 7

演算法 壓縮 (MiB/s) 解壓縮 (MiB/秒) 解壓縮作業最糟的延遲時間 (ns) 總大小 (MiB)
Zstd(-7) 752.1470396 1446.748532 9302.3125 18.61393929
minilzo 1212.400326 2135.49407 6817.839844 16.16410065
lz4(1) 1389.675715 2920.094092 6355.849609 17.21983242
lz4(11) 1801.55426 3136.318174 5255.708984 19.92521095
WKdm 1986.560443 3119.523406 3095.283203 21.98047638

ARM A53

演算法 壓縮 (MiB/s) 解壓縮 (MiB/秒) 解壓縮作業最糟的延遲時間 (ns) 總大小 (MiB)
Zstd(-7) 189.3189527 486.6744277 29856.93359 18.61393929
minilzo 317.4546381 528.877112 15633.99219 16.16410065
lz4(1) 294.5410547 1127.360347 12683.55273 17.21983242
lz4(11) 378.2672263 1247.749072 12452.67676 19.92521095
WKdm 337.6087322 471.9796684 14254.39453 21.98047638

ARM A73

演算法 壓縮 (MiB/s) 解壓縮 (MiB/秒) 解壓縮作業最糟的延遲時間 (ns) 總大小 (MiB)
Zstd(-7) 284.3621221 623.15235 20958.90332 18.61393929
minilzo 487.4546545 747.9218419 16784.83105 16.16410065
lz4(1) 441.5927551 1159.839867 10007.28418 17.21983242
lz4(11) 573.7741984 1240.964187 9987.875 19.92521095
WKdm 639.5418085 597.6976995 13867.47266 21.98047638

壓縮儲存空間

我們使用相同的 64 MiB 測試資料集,評估所提議的壓縮儲存系統。在本案例中,我們以兩種模式進行評估,分別是只包含左側和右側版位的雙頁系統,以及建議的三個版位系統。這項研究的目的,是嘗試量化在真正簡單的兩個插槽系統中加入中間插槽的複雜性效益。

LZ4 是建議的壓縮演算法,用於產生要儲存的壓縮資料,加速因子設為 11。也就是說,在 16384 個輸入頁面中,有 12510 個產生需要儲存的資料,而 3874 個則未經壓縮。

吃角子老虎 儲存空間的頁面 總 MiB 節省的 MiB 壓縮比
兩個插槽 6255 39.5 24.4 1.62
三個插槽 4276 31.8 32.1 2.01

請注意,6255 正好是 12510 的一半,表示每個頁面都有一個 buddy,也就是說,對於 LZ4 輸入內容,已達到此儲存策略的最佳壓縮比。

既有技術與參考資料

所有主要的現代作業系統 (Windows、macOS、Linux 和 Linux 衍生版本,例如 CastOS 等) 都支援記憶體壓縮功能,可做為磁碟交換的替代方案或補充方案。

三頁儲存策略的靈感來自 Linux zbud 和 z3fold 記憶體管理系統。