[譯] 什麼是快取 false sharing 以及如何解決(Golang 示例)

咔嘰咔嘰發表於2019-06-15

在解釋快取 false sharing 之前,有必要簡要介紹一下快取在 CPU 架構中的工作原理。

CPU 中快取的最小化單位是快取行(現在來說,CPU 中常見的快取行大小為 64 位元組)。因此,當 CPU 從記憶體中讀取變數時,它將讀取該變數附近的所有變數。圖 1 是一個簡單的例子:

圖1

當 core1 從記憶體中讀取變數 a 時,它會同時將變數 b 讀入快取。(順便說一下,我認為 CPU 從記憶體中批量讀取變數的主要原因是基於空間區域性性理論:當 CPU 訪問一個變數時,它可能很快就會讀取它旁邊的變數。)(譯者注:關於空間區域性性理論可以參考這篇文章

該快取架構存在一個問題:如果一個變數存在於不同 CPU 核心中的兩個快取行中,如圖 2 所示:

圖2

當 core1 更新變數 a 時:

圖3

當 core2 讀取變數 b 時,即使變數 b 未被修改,它也會使 core2 的快取未命中。所以 core2 會從記憶體中重新載入快取行中的所有變數,如圖 4 所示:

圖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,我相信你會得到相同的結果。

在將其應用於你的程式碼之前,應該瞭解兩個要點:

  1. 確保系統中 CPU 的快取行大小:這與你使用的快取填充大小有關。
  2. 填充更多變數意味著消耗更多記憶體資源。在你的方案中執行基準測試以確保這些記憶體消耗是值得的。

我的所有示例程式碼都在GitHub上。

相關文章