偽共享

什麼是偽共享

偽共享與快取機制有關,用於維護儲存在多個 CPU 快取中的一個快取行的資料一致性;其學術定義見[1]。考慮一個包含引用計數和字串的結構體

struct foo {
        refcount_t refcount;
        ...
        char name[16];
} ____cacheline_internodealigned_in_smp;

成員 ‘refcount’(A) 和 ‘name’(B) _共享_ 一個快取行,如下所示

              +-----------+                     +-----------+
              |   CPU 0   |                     |   CPU 1   |
              +-----------+                     +-----------+
             /                                        |
            /                                         |
           V                                          V
       +----------------------+             +----------------------+
       | A      B             | Cache 0     | A       B            | Cache 1
       +----------------------+             +----------------------+
                           |                  |
---------------------------+------------------+-----------------------------
                           |                  |
                         +----------------------+
                         |                      |
                         +----------------------+
            Main Memory  | A       B            |
                         +----------------------+

‘refcount’ 經常被修改,但 ‘name’ 在物件建立時設定一次,之後不再修改。當多個 CPU 同時訪問 ‘foo’ 時,‘refcount’ 僅由一個 CPU 頻繁地遞增,而 ‘name’ 被其他 CPU 讀取,由於 “共享”,所有讀取 CPU 都必須一遍又一遍地重新載入整個快取行,即使 ‘name’ 從未更改。

有很多現實世界中因偽共享導致效能下降的案例。其中之一是 rw_semaphore ‘mmap_lock’ 在 mm_struct 結構體中,其快取行佈局的改變引發了效能下降,Linus 在 [2] 中對此進行了分析。

有害的偽共享有兩個關鍵因素

  • 一個被多個 CPU 訪問(共享)的全域性資料

  • 在對資料的併發訪問中,至少有一個寫操作:寫/寫或寫/讀情況。

共享可能來自完全不相關的核心元件,或同一核心元件的不同程式碼路徑。

偽共享的陷阱

在過去,當一個平臺只有一到幾個 CPU 時,熱資料成員可能會被特意放在同一個快取行中,以使它們快取命中率更高,並節省快取行/TLB。但對於最近擁有數百個 CPU 的大型系統來說,當鎖爭用激烈時,這可能不起作用,因為鎖持有者 CPU 可能會寫入資料,而其他 CPU 則忙於自旋等待鎖。

回顧過去的案例,偽共享有幾種常見的模式

  • 鎖(自旋鎖/互斥鎖/訊號量)和受其保護的資料被特意放在一個快取行中。

  • 全域性資料被放在一個快取行中。一些核心子系統有許多小尺寸(4 位元組)的全域性引數,這些引數很容易被組合在一起並放入一個快取行中。

  • 大型資料結構的成員隨機地放在一起而沒有被注意到(快取行通常為 64 位元組或更多),例如 ‘mem_cgroup’ 結構體。

下面的 “緩解措施” 部分提供了真實的例子。

除非有意識地進行檢查,否則很容易發生偽共享,因此對於效能關鍵型工作負載,執行特定的工具來檢測影響效能的偽共享案例並進行相應的最佳化非常有價值。

如何檢測和分析偽共享

perf record/report/stat 被廣泛用於效能調優,一旦檢測到熱點,可以使用 ‘perf-c2c’ 和 ‘pahole’ 等工具進一步檢測和精確定位可能的偽共享資料結構。當存在多層行內函數時,‘addr2line’ 也擅長解碼指令指標。

perf-c2c 可以捕獲具有最多偽共享命中的快取行、訪問該快取行的解碼函式(檔案行號)以及資料的內聯偏移量。簡單的命令是

$ perf c2c record -ag sleep 3
$ perf c2c report --call-graph none -k vmlinux

在測試期間執行上述命令將-it-scale 的 tlb_flush1 案例時,perf 報告類似以下內容

Total records                     :    1658231
Locked Load/Store Operations      :      89439
Load Operations                   :     623219
Load Local HITM                   :      92117
Load Remote HITM                  :        139

#----------------------------------------------------------------------
    4        0     2374        0        0        0  0xff1100088366d880
#----------------------------------------------------------------------
  0.00%   42.29%    0.00%    0.00%    0.00%    0x8     1       1  0xffffffff81373b7b         0       231       129     5312        64  [k] __mod_lruvec_page_state    [kernel.vmlinux]  memcontrol.h:752   1
  0.00%   13.10%    0.00%    0.00%    0.00%    0x8     1       1  0xffffffff81374718         0       226        97     3551        64  [k] folio_lruvec_lock_irqsave  [kernel.vmlinux]  memcontrol.h:752   1
  0.00%   11.20%    0.00%    0.00%    0.00%    0x8     1       1  0xffffffff812c29bf         0       170       136      555        64  [k] lru_add_fn                 [kernel.vmlinux]  mm_inline.h:41     1
  0.00%    7.62%    0.00%    0.00%    0.00%    0x8     1       1  0xffffffff812c3ec5         0       175       108      632        64  [k] release_pages              [kernel.vmlinux]  mm_inline.h:41     1
  0.00%   23.29%    0.00%    0.00%    0.00%   0x10     1       1  0xffffffff81372d0a         0       234       279     1051        64  [k] __mod_memcg_lruvec_state   [kernel.vmlinux]  memcontrol.c:736   1

關於 perf-c2c 的一個很好的介紹是 [3]

‘pahole’ 解碼以快取行粒度分隔的資料結構佈局。使用者可以將 perf-c2c 輸出中的偏移量與 pahole 的解碼匹配,以定位確切的資料成員。對於全域性資料,使用者可以在 System.map 中搜索資料地址。

可能的緩解措施

偽共享並不總是需要緩解。偽共享緩解措施應平衡效能提升與複雜性和空間消耗。有時,較低的效能是可以接受的,沒有必要過度最佳化每個很少使用的資料結構或冷資料路徑。

隨著核心數量的增加,越來越多地看到偽共享損害效能的情況。由於這些不利影響,已經提出了各種子系統(如網路和記憶體管理)的許多補丁並已合併。一些常見的緩解措施(帶有示例)是

  • 將熱全域性資料分離到其自身的專用快取行中,即使它只是一個 ‘short’ 型別。缺點是會消耗更多的記憶體、快取行和 TLB 條目。

  • 重新組織資料結構,將相互干擾的成員分離到不同的快取行中。一個缺點是它可能會引入其他成員的新偽共享。

  • 儘可能用 ‘read’ 替換 ‘write’,尤其是在迴圈中。例如,對於一些全域性變數,使用 compare(read)-then-write 代替無條件寫入。例如,使用

    if (!test_bit(XXX))
            set_bit(XXX);
    

    代替直接 “set_bit(XXX);”,對於 atomic_t 資料也是如此

    if (atomic_read(XXX) == AAA)
            atomic_set(XXX, BBB);
    
  • 儘可能將熱全域性資料轉換為 ‘per-cpu data + global data’,或合理地增加將 per-cpu 資料同步到全域性資料的閾值,以減少或推遲對該全域性資料的 ‘write’。

當然,應仔細驗證所有緩解措施,以避免產生副作用。為了在編碼時避免引入偽共享,最好

  • 注意快取行邊界

  • 將大多數只讀欄位組合在一起

  • 將同時寫入的內容組合在一起

  • 將經常讀取和經常寫入的欄位分離到不同的快取行中。

最好添加註釋說明偽共享的考慮。

需要注意的是,有時即使在檢測到並解決了嚴重的偽共享之後,效能也可能沒有明顯的改善,因為熱點會轉移到新的位置。

其他

一個懸而未決的問題是,核心有一個可選的資料結構隨機化機制,該機制也會隨機化資料成員之間快取行共享的情況。