- 原文地址:medium.com/@genchilu/w…
- 原文作者:Genchi Lu
- 譯文地址:github.com/watermelo/d…
- 譯者:咔嘰咔嘰
- 譯者水平有限,如有翻譯或理解謬誤,煩請幫忙指出
在解釋快取 false sharing 之前,有必要簡要介紹一下快取在 CPU 架構中的工作原理。
CPU 中快取的最小化單位是快取行(現在來說,CPU 中常見的快取行大小為 64 位元組)。因此,當 CPU 從記憶體中讀取變數時,它將讀取該變數附近的所有變數。圖 1 是一個簡單的例子:
當 core1 從記憶體中讀取變數 a 時,它會同時將變數 b 讀入快取。(順便說一下,我認為 CPU 從記憶體中批量讀取變數的主要原因是基於空間區域性性理論:當 CPU 訪問一個變數時,它可能很快就會讀取它旁邊的變數。)(譯者注:關於空間區域性性理論可以參考這篇文章)
該快取架構存在一個問題:如果一個變數存在於不同 CPU 核心中的兩個快取行中,如圖 2 所示:
當 core1 更新變數 a 時:
當 core2 讀取變數 b 時,即使變數 b 未被修改,它也會使 core2 的快取未命中。所以 core2 會從記憶體中重新載入快取行中的所有變數,如圖 4 所示:
這就是快取 false sharing:一個 CPU 核更新變數會強制其他 CPU 核更新快取。而我們都知道從快取中讀取 CPU 的變數比從記憶體中讀取變數要快得多。因此,雖然該變數一直存在於多核中,但這會顯著影響效能。
解決該問題的常用方法是快取填充:在變數之間填充一些無意義的變數。使一個變數單獨佔用 CPU 核的快取行,因此當其他核更新時,其他變數不會使該核從記憶體中重新載入變數。
我們使用如下的 Go 程式碼來簡要介紹快取 false sharing 的概念。
這是一個帶有三個 uint64 變數的結構體,
type NoPad struct {
a uint64
b uint64
c uint64
}
func (myatomic *NoPad) IncreaseAllEles() {
atomic.AddUint64(&myatomic.a, 1)
atomic.AddUint64(&myatomic.b, 1)
atomic.AddUint64(&myatomic.c, 1)
}
複製程式碼
這是另一個結構,我使用 [8]uint64 來做快取填充:
type Pad struct {
a uint64
_p1 [8]uint64
b uint64
_p2 [8]uint64
c uint64
_p3 [8]uint64
}
func (myatomic *Pad) IncreaseAllEles() {
atomic.AddUint64(&myatomic.a, 1)
atomic.AddUint64(&myatomic.b, 1)
atomic.AddUint64(&myatomic.c, 1)
}
複製程式碼
然後寫一個簡單的程式碼來執行基準測試:
func testAtomicIncrease(myatomic MyAtomic) {
paraNum := 1000
addTimes := 1000
var wg sync.WaitGroup
wg.Add(paraNum)
for i := 0; i < paraNum; i++ {
go func() {
for j := 0; j < addTimes; j++ {
myatomic.IncreaseAllEles()
}
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkNoPad(b *testing.B) {
myatomic := &NoPad{}
b.ResetTimer()
testAtomicIncrease(myatomic)
}
func BenchmarkPad(b *testing.B) {
myatomic := &Pad{}
b.ResetTimer()
testAtomicIncrease(myatomic)
}
複製程式碼
使用 2014 年的 MacBook Air 做的基準測試結果如下:
$> go test -bench=.
BenchmarkNoPad-4 2000000000 0.07 ns/op
BenchmarkPad-4 2000000000 0.02 ns/op
PASS
ok 1.777s
複製程式碼
基準測試的結果表明它將效能從 0.07 ns/op 提高到了 0.02 ns/op,這是一個很大的提高。
你也可以用其他語言測試這個,比如 Java,我相信你會得到相同的結果。
在將其應用於你的程式碼之前,應該瞭解兩個要點:
- 確保系統中 CPU 的快取行大小:這與你使用的快取填充大小有關。
- 填充更多變數意味著消耗更多記憶體資源。在你的方案中執行基準測試以確保這些記憶體消耗是值得的。
我的所有示例程式碼都在GitHub上。