自訂記憶體配置器

攤提成本與最壞情況延遲的權衡

假設你早上通勤平均 20 分鐘,聽起來不錯——直到某天一條路封閉害你花了 90 分鐘、錯過了一場會議。那個平均是誠實的,卻藏起了一次罕見而痛苦的尖峰。配置器也有同樣的兩面性格。攤提成本(amortized cost)是長期下來每個操作的平均成本;最壞情況延遲(worst-case latency)是任何單一操作可能花的最長時間。配置器設計的深層權衡在於:那些讓平均美妙地便宜的技巧,可能藏著罕見、極為昂貴的操作——而對某些程式而言,那罕見的尖峰才是真正要緊的。

把機制具體說清楚。一個快速配置器的多數 malloc 與 free 呼叫幾乎免費——取下或推入一條執行緒區域自由串列,幾條指令而已。但偶爾某一次呼叫會觸發大工程:執行緒快取空了,於是它得取中央鎖並批次補充;中央堆積空了,於是它得用 mmap() 向作業系統要更多記憶體(一個系統呼叫,慢);或是跑一趟合併或清理。攤提分析把那些罕見大工程的成本分攤到中間許多便宜的呼叫上,於是每次呼叫的平均維持很低。就吞吐量而言,這確實是正確的思考方式。但延遲不會平均:那個倒楣、撞上 mmap() 或鎖或清理的單一呼叫,可能比正常的長上數百或數千倍。量測平均延遲,配置器看起來很棒;量測 99.9 百分位(尾端)延遲,那偶發的尖峰就清楚現形。

為何這個區別不是學術空談:對一個只需要快點跑完的批次程式,攤提(平均)成本正是你在乎的,偶爾較高的成本是看不見的。但對一個互動式服務、一個交易系統、或任何即時系統,請求過程中單一次 5 毫秒的停頓就是一個真實、使用者看得見的失敗,即使平均是微秒等級——尾端延遲才是該看的指標。這正是為什麼即時與低延遲系統往往在熱路徑上完全避開通用配置器,改用 pool 或 arena 預先配置,讓任何一次 malloc 呼叫都不可能誤入慢速路徑。誠實的總結:攤提與最壞情況是不同的問題,一個配置器可能擅長其一、卻拙於另一,而你必須清楚你的程式實際在問哪一個。

/* 1000 次 malloc:999 次約 20 ns(執行緒快取取出) */ /* 1 次撞上空快取 -> mmap() 系統呼叫 -> 約 50000 ns */ /* 攤提(平均) ~= (999*20 + 50000)/1000 ~= 70 ns <- 看起來沒事 */ /* 最壞情況延遲 == 50000 ns <- 那道尖峰 */

漂亮的平均可能藏著一道罕見的巨大尖峰;吞吐量在乎平均,即時系統在乎尖峰。

低的平均(攤提)成本並不為最壞情況延遲設下上界。即時與低延遲程式常用 pool 或 arena 預先配置,正是為了讓任何 malloc 都不會在請求中途撞上慢速路徑。

又称
amortized costtail latencyworst-case latency攤提成本尾端延遲最壞情況延遲