後端工坊 2026 年 9 月 4 日

2026-09-04 — Rust 1.98.1 修補 vtable 空指標、Linux 記憶體分層交給 DAMON 自動化、Go map 的 Swiss Table 拆解

primary=https://blog.rust-lang.org/2026/09/03/Rust-1.98.1/ primary=https://github.com/rust-lang/rust/releases/tag/1.98.1 primary=https://github.com/rust-lang/rust/issues/161441 primary=https://lwn.net/Articles/1092001/ primary=https://lwn.net/Articles/1071256/ primary=https://victoriametrics.com/blog/go-swiss-table-map/ primary=https://go.dev/blog/swisstable

Rust 1.98.1 緊急修補:非同步 trait object 的 vtable 曾被寫成空指標

Rust Blog · 2026-09-03

Rust 團隊在 2026-09-03 釋出 1.98.1 修補版本,針對三週前(2026-08-20)發布的 1.98.0 中一個會讓 trait object 虛擬函式表(vtable)產生錯誤空指標的編譯器缺陷完成修補。該問題編號為 #161441,官方公告見 Rust Blog。只要專案使用複雜的非同步 trait object 模式,升級到 1.98.0 後就有機會在執行期直接觸發 segmentation fault。

背景:非同步 trait object 為何會走到 vtable

Rust 的 dyn Trait 透過 vtable 在執行期分派方法呼叫,每個方法對應 vtable 裡一個函式指標欄位。當某個方法的 trait bound 被編譯器判定「不可能成立」時,rustc 會把該欄位標記為 VtblEntry::Vacant,正常情況下這種方法本來就不會被呼叫,留空指標無妨。受影響的程式碼型態,是把非同步方法封裝進 boxed trait object 的「type-erased async trait」寫法,常見於分層包裝的服務,例如在類似 UpgradeService 的中介層外再包一層 boxed dyn Future。這種巢狀泛型 trait object 讓 predicate 求解器在 1.98.0 裡誤判某個原本會被呼叫的方法為不可能達成的路徑。

核心改動:被誤判為不可能的方法

根據 issue #161441 的描述,rustc 在生成 vtable 時,對承載複雜泛型限制的非同步方法(例如具體實作 EagerHttpProxyConnector::serve)錯誤判斷其 predicate 不可滿足,因而把該 slot 寫成 Vacant 而非真正的函式指標。實際執行到該方法時,vtable 查到的是空指標,一旦被解參考呼叫就是未定義行為,多半直接 segfault。這個缺陷只存在於 1.98.0,1.97.1 及更早版本不受影響,官方回報 nightly-2026-07-16 之後的建置已經修正。1.98.1 把該修正回植(backport)到 stable 分支,對應提交為 48a229c

影響範圍:哪些專案該升級

受影響範圍集中在大量使用「boxed 非同步 trait」抽象的服務端程式碼,尤其是把多層 middleware/service 包裝成 Box<dyn Service> 的寫法,這種模式在仿 Tower 架構的 Rust 非同步網路框架裡相當常見。只要專案在 1.98.0 上編譯過帶有這類巢狀泛型 async trait object 的程式碼,就有必要立刻升級,而不是等下一個常規版本。升級方式維持 rustup update stable,由於這是 patch release,不含 1.98.0 之外的其他語言或函式庫變動。目前官方公告與 issue 討論串都未附上可重現的最小範例原始碼,只描述了觸發模式與受影響版本區間。

原始來源:Rust Blog: Announcing Rust 1.98.1GitHub Release 1.98.1GitHub Issue #161441


Linux 記憶體分層最新戰線:DAMON 接手冷熱頁搬遷,CXL 頻寬也要用好用滿

LWN.net · 2026-09-03

LWN.net 在 2026-09-03 刊出文章,彙整 Linux 核心記憶體分層(memory tiering)子系統近期的開發進度,主軸是把原本仰賴 NUMA balancing 的冷熱頁判斷,逐步交給 DAMON/DAMOS 監控框架自動處理,搬遷對象是 DRAM 與 CXL 掛載擴充記憶體之間的頁面。原文參見 LWN.net,機制脈絡另可見同站稍早的 DAMON 進度整理

背景:分層、抽象距離與冷熱頁搬遷

Linux 核心從 6.1 版才有正式的記憶體分層概念:每個 NUMA node 會被指派一個「抽象距離」(abstract distance),數字越小代表存取效能越好,一般 DRAM node 預設是 512,分層演算法再依距離把 node 分進不同 tier,預設 tier 的 rank 值是 200當快速記憶體不夠用時,核心會把近期不常存取的頁面「降級」(demotion)搬到較慢的 tier,反過來偵測到慢速 tier 裡有頁面被頻繁存取,則會「升級」(promotion)搬回快速記憶體。promotion 的判斷比 demotion 難處理得多,早期作法是借用 NUMA balancing 既有的掃描機制,用取樣方式猜測慢速節點上哪些頁面正在被使用。DAMON(Data Access MONitor)要取代的正是這種借用機制——它由一條核心執行緒每 5ms 取樣一次存取行為,每 100ms 把結果彙總給使用者空間,量測到的額外負擔低於 0.1%

核心改動:DAMOS 接手搬遷決策,CXL 頻寬也被排進配置

建立在 DAMON 監控之上的 DAMOS(DAMON-based Operation Schemes)負責依存取模式下達實際操作指令,例如強制換出冷頁、或是把頁面在不同記憶體層之間搬動。近期併入主線的相關項目包括:

  • TPP-DAMON(Transparent Page Placement):讓 DAMON 自動在系統 RAM 與 CXL 掛載記憶體之間做熱頁升級、冷頁降級,已於 6.16 併入主線,6.19 再補上 control-group 感知能力。
  • 動態交錯配置(dynamic interleaving)擴充:刻意把一部分「熱」記憶體配置到頻寬尚有餘裕、但延遲較高的 CXL 記憶體上,藉此拉高整體記憶體頻寬使用率,已於 6.17 併入。
  • 自動調校(automatic tuning):讓上述機制不需要人工設定門檻值就能運作,目標併入 7.1-rc1

這一系列改動的共通點,是把過去需要管理者手動設定 DAMOS 規則門檻的部分逐步自動化,同時把 CXL 記憶體從「純容量擴充」升級成「也能貢獻頻寬」的角色。各項目分屬不同核心版本,代表要拿到完整功能得等發行版核心追上 6.19 甚至 7.1 之後的版本。

影響範圍:誰會先感受到差異

公開的效能數字顯示,TPP-DAMON 在一個 llama.cpp 推論負載測試中量到 94% 的改善幅度,動態交錯配置在另一組測試裡則帶來 25% 的加速,兩者都是記憶體容量或頻寬吃緊、但延遲敏感度較低的工作負載。這類優化的第一批受益者會是配置了 CXL 擴充記憶體的雲端與大型記憶體機器,尤其是大型語言模型推論、記憶體內資料庫這類熱資料集遠大於單顆 DRAM 容量的場景。由於功能分散在 6.167.1-rc1 多個版本才逐步到齊,現階段要用上完整路徑仍得自行編譯較新的主線核心,或等待各發行版陸續回植。

原始來源:LWN.net: Recent work in memory tieringLWN.net: A 2026 DAMON update


Go 內建 map 的 Swiss Table 拆解:控制位元組怎麼讓查找一次跳過整組 8 格

VictoriaMetrics Blog · 2026-09-03

VictoriaMetrics 官方部落格工程師 Phuong Le 在 2026-09-03 發表長文,逐層拆解 Go 內建 map 型別自 Go 1.24 起改用的 Swiss Table 雜湊表設計,對照舊版以桶(bucket)為單位、靠 overflow 指標串接的實作,並提到 Go 1.27 一項把 key 與 value 陣列拆開存放的實驗性佈局。原文見 VictoriaMetrics Blog,設計出處可回溯到 Go 官方的 Faster Go maps with Swiss Tables

原本的問題:overflow bucket 逼出的相依性指標載入

Go 1.24 之前的 map 用固定 8 格的 bucket 當儲存單位,一個 bucket 塞滿後,新資料會寫進以指標串接的 overflow bucket。查找一個不存在的 key 時,程式得先載入 overflow 指標才能知道下一個 8 格在哪裡,這個指標載入是相依的——CPU 沒辦法把它跟前一個 bucket 的比較平行做掉,每多一層 overflow 就多一次無法重疊的記憶體延遲。bucket 之間也不保證彼此在記憶體中相鄰,對快取不友善。Swiss Table 要解決的正是這個痛點:把探測序列(probe sequence)限制在一塊連續配置的陣列裡,不必依賴逐層載入的指標就能推進。

設計/機制細節:控制位元組與 SIMD 一次比對 8 格

Swiss Table 把儲存單位換成「group」,每個 group 固定放 8 組 key/value,外加一個 8 bytes 的控制位元組,打包進一個 uint64:

type group struct {
    ctrl  uint64
    slots [8]struct {
        key  Key
        elem Elem
    }
}

每個 slot 對應一個控制位元組,最高位是 0 代表這格有資料、是 1 代表特殊狀態,資料格的低 7 bit 存的是雜湊值切出來的 H2 片段;64-bit 雜湊值的高 57 bit 則叫 H1,用來選要進哪個 group。查找時,AMD64 上用 SIMD 指令把目標 key 的 H2 同時跟 8 個控制位元組比較,一次算出一個 bitmap 標出可能命中的位置,非 SIMD 架構則用整個 uint64 的位元運算達到等效效果。這一步把「逐格比較」壓成「一次比較整組」,原本 overflow 鏈上才需要的指標載入,在 Swiss Table 裡換成同一塊陣列內的三角形探測序列(triangular probing),每個 group 保證只造訪一次。刪除資料時,若整個 group 還有空格就直接標成 empty(10000000);若 group 已滿則要標成墓碑(tombstone,11111110)以免打斷後續探測。

實際效果:table 分裂與 Go 1.27 的拆欄佈局

新舊設計的差異可以整理成:

項目舊版 bucket 設計(<1.24)Swiss Table 設計(≥1.24)
儲存單位相鄰性bucket 間靠指標串接,不保證相鄰group 陣列連續配置
找不到 key 的成本逐層載入 overflow 指標同陣列內三角形探測
比對方式逐格比較 tophashSIMD 一次比較 8 個控制位元組
負載係數上限約 6.5/8,依情境浮動固定 7/8(87.5%)

容量成長路徑是從 1 個 group 倍增到 128 個 group(即 1024 格),超過這個門檻後不再單純長大單一 table,而是靠 directory(以 H1 高位索引)把資料切成多個獨立 table,一次只重新分佈受影響的那個 table。這個做法讓官方量到 map 操作最高比 Go 1.23 快 60%,實際應用程式的 CPU 使用量幾何平均改善約 1.5%Go 1.27 額外提供 GOEXPERIMENT=mapsplitgroup,把 group 裡的 key 陣列和 value 陣列拆開存放而非交錯排列,對 map[int64]struct{} 這類 value 極小或零大小的情境,單一 group 可省下 56 bytes 的 padding,查找也只在命中後才需要載入 value 那一側。

原始來源:VictoriaMetrics Blog: How Swiss Tables Work in Go's Built-in MapGo Blog: Faster Go maps with Swiss Tables


End of article
0
Would love your thoughts, please comment.x
()
x