在之前的講座中,我們討論了什麼是錯誤共享,以及它如何影響系統效能。本次講座將探討幾種可以避免錯誤共享的方法,以確保系統效能不會下降。
三種避免錯誤共享的方法
我們將重點介紹以下三種方法。當然,還有其他方法,但我們將集中討論以下三種:
- 填充 (Padding)
- 別名 (Aliasing)
- 避免相鄰更新 (Avoid Adjacent Updates)
填充 (Padding)
填充是指在相鄰的資料之間插入額外的空間,以確保它們位於不同的快取行 (Cache Line) 中。
-
問題: 假設有兩個獨立的資料 D1 和 D2,它們被分配到同一快取行中的記憶體。即使它們是獨立的,當不同的執行緒嘗試同時存取這兩個變數時,仍會發生錯誤共享,導致系統效能下降。
-
解決方案: 在 D1 和 D2 之間添加填充。如果在 D1 被分配記憶體後,插入一個 64 位元組的字元陣列(假設快取行大小為 64 位元組),就可以確保 D2 不會被分配到同一快取行中的記憶體。
-
範例:
-
D1 被分配到位址 0-3。
-
插入一個 64 位元組的填充字元。
-
D2 被分配到填充之後的記憶體。
-
-
優點: 確保 D1 和 D2 位於不同的快取行中,允許多個執行緒同時存取它們,而不會發生快取失效 (Cache Invalidation) 的問題。
-
缺點: 這種方法會浪費大量的記憶體。如果結構或類別的大小很大,則需要添加大量的填充,導致記憶體使用效率降低。
別名 (Aliasing)
別名是一種確保變數被分配到不同快取行中的方法。
-
方法: 使用特定的語法(例如
alignas(64)
),可以在編譯時或執行時指示編譯器或運行環境,將變數分配到不同的快取行中。 -
範例: 使用
alignas(64)
可以確保 D1 和 D2 被分配到不同的 64 位元組快取行中。 -
優點: 透過別名,可以確保 D1 位於一個快取行中,而 D2 位於另一個快取行中。即使一個快取行失效,另一個執行緒仍然可以存取 D2,因為它位於一個完全不同的快取行中。
避免相鄰更新 (Avoid Adjacent Updates)
避免相鄰更新是指避免頻繁更新相鄰的變數,因為它們可能位於同一快取行中。
-
問題: 如果兩個變數 D1 和 D2 相鄰,並且被頻繁地更新,則會導致快取行不斷失效,影響系統效能。
-
解決方案: 避免在短時間內頻繁地更新相鄰的變數。
-
方法:
-
在更新 D1 和 D2 之前,先執行所有的計算。
-
在所有資料都準備好後,一次性更新 D1 和 D2。
-
避免以極短的時間間隔(例如每秒或每毫秒)更新這些變數。
-
-
原因: D1 和 D2 很可能位於同一快取行中,頻繁的更新會導致快取不斷失效,降低系統效能。
實驗問題
以下是一個您可以嘗試的實驗,以驗證這些方法的有效性:
- 建立資料結構: 定義一個包含兩個成員變數 D1 和 D2 的資料結構。
- 建立執行個體: 建立該資料結構的一個執行個體 D,並初始化 D1 和 D2。
- 建立更新函數: 建立一個函數
fun
,該函數將 D1 或 D2 作為參數,並將其更新 1000 或 10000 次。 -
測量時間:
-
在不使用填充或別名的情況下,測量更新 D1 和 D2 所需的時間。
-
使用填充(插入一個 64 位元組的字元陣列)或別名,然後再次測量更新 D1 和 D2 所需的時間。
- 比較結果: 比較兩種情況下的時間,以確定填充或別名是否提高了系統效能。
-
預計在使用填充或別名的情況下,系統效能會更好,因為這可以避免錯誤共享。
這個實驗可以很容易地實現,並提供對錯誤共享影響的直觀理解。