天天看點

什麼是僞共享?又該怎樣避免僞共享的問題?

CPU 如何讀寫資料的?

先來認識 CPU 的架構,隻有了解了 CPU 的 架構,才能更好地了解 CPU 是如何讀寫資料的,對于現代 CPU 的架構圖如下:

什麼是僞共享?又該怎樣避免僞共享的問題?

可以看到,一個 CPU 裡通常會有多個 CPU 核心是,比方上圖中的 1 号和 2 号 CPU 核心,并且每個 CPU 核心都有自己的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分為 dCache(資料緩存) 和 iCache(指令緩存),L3 Cache 則是多個核心共享的,這就是 CPU 典型的緩存層次。

上面提到的都是 CPU 内部的 Cache,放眼外部的話,還會有記憶體和硬碟,這些存儲設施共同構成了金字塔存儲層次。如下圖所示:

什麼是僞共享?又該怎樣避免僞共享的問題?

從上圖也可以看到,從上往下,存儲設施的容量會越大,而通路速度會越慢。至于每個存儲設施的通路延時,你可以看下圖的表格:

什麼是僞共享?又該怎樣避免僞共享的問題?

你可以看到, CPU 通路 L1 Cache 速度比通路記憶體快 100 倍,這就是為什麼 CPU 裡會有 L1~L3 Cache 的起因,目的就是把握 Cache 作為 CPU 與記憶體之間的緩存層,以減少對記憶體的通路頻率。

CPU 從記憶體中讀取資料到 Cache 的時候,并不是一個位元組一個位元組讀取,而是一塊一塊的方式來讀取資料的,這一塊一塊的資料被稱為 CPU Line(緩存行),是以 CPU Line 是 CPU 從記憶體讀取資料到 Cache 的機關。

至于 CPU Line 大小,在 Linux 系統可以用下面的方式檢視到,你可以看我伺服器的 L1 Cache Line 大小是 64 位元組,也就意味着 L1 Cache 一次載入資料的大小是 64 位元組。

什麼是僞共享?又該怎樣避免僞共享的問題?

那麼對數組的加載, CPU 就會加載數組裡面連續的多個資料到 Cache 裡,因而我們應該按照實體記憶體位址分布的順序去通路元素,這樣通路數組元素的時候,Cache 命中率就會很高,于是就能減少從記憶體讀取資料的頻率, 進而可以提高程式的性能。

但是,在我們不使用數組,而是使用單獨的變量的時候,則會有 Cache 僞共享的問題,Cache 僞共享問題上是一個性能殺手,我們應該要規避它。

接下來,就來看看 Cache 僞共享是什麼?又如何避免這個問題?

現在假設有一個雙核心的存在 CPU,這兩個 CPU 核心并行運作着兩個不同的線程,它們同時從記憶體中讀取兩個不同的資料,分别是類型為 long 的變量 A 和 B,這個兩個資料的位址在實體記憶體上是連續的,假如 Cahce Line 的大小是 64 位元組,并且變量 A 在 Cahce Line 的開頭位置,那麼這兩個資料是位于同一個位置 Cache Line 中,又由于 CPU Line 是 CPU 從記憶體讀取資料到 Cache 的機關,是以這兩個資料會被同時讀入到了兩個 CPU 核心中各自 Cache 中。

什麼是僞共享?又該怎樣避免僞共享的問題?

我們來思考一個問題,假如這兩個核心的線程分别修改不同的資料,比方 1 号 CPU 核心的線程隻修改了 變量 A,或者 2 号 CPU 核心的線程的線程隻修改了變量 B,會發生什麼呢?

分析僞共享的問題

現在我們結合保證多核緩存一緻的 MESI 協定,來說明這一整個的過程,假如你還不知道 MESI 協定,你可以看我這篇文章「10 張圖打開 CPU 緩存一緻性的大門」。

①. 最開始變量 A 和 B 都還不在 Cache 裡面,假設 1 号核心綁定了線程 A,2 号核心綁定了線程 B,線程 A 隻會讀寫變量 A,線程 B 隻會讀寫變量 B。

什麼是僞共享?又該怎樣避免僞共享的問題?

②. 1 号核心讀取變量 A,因為 CPU 從記憶體讀取資料到 Cache 的機關是 Cache Line,也正好變量 A 和 變量 B 的資料歸屬于同一個 Cache Line,是以 A 和 B 的資料都會被加載到 Cache,并将此 Cache Line 标記為「獨占」狀态。

什麼是僞共享?又該怎樣避免僞共享的問題?

③. 接着,2 号核心開始從記憶體裡讀取變量 B,同樣的也是讀取 Cache Line 大小的資料到 Cache 中,此 Cache Line 中的資料也包含了變量 A 和 變量 B,此時 1 号和 2 号核心的 Cache Line 狀态變為「共享」狀态。

什麼是僞共享?又該怎樣避免僞共享的問題?

④. 1 号核心需要修改變量 A,發現此 Cache Line 的狀态是「共享」狀态,是以先需要通過總線發送消息給 2 号核心,通知 2 号核心把 Cache 中對應的 Cache Line 标記為「已失效」狀态,而後 1 号核心對應的 Cache Line 狀态變成「已修改」狀态,并且修改變量 A。

什麼是僞共享?又該怎樣避免僞共享的問題?

⑤. 之後,2 号核心需要修改變量 B,此時 2 号核心的 Cache 中對應的 Cache Line 是已失效狀态,另外因為 1 号核心的 Cache 也有此相同的資料,且狀态為「已修改」狀态,是以要先把 1 号核心的 Cache 對應的 Cache Line 寫回到記憶體,而後 2 号核心再從記憶體讀取 Cache Line 大小的資料到 Cache 中,最後把變量 B 修改到 2 号核心的 Cache 中,并将狀态标記為「已修改」狀态。

什麼是僞共享?又該怎樣避免僞共享的問題?

是以,可以發現假如 1 号和 2 号 CPU 核心這樣持續交替的分别修改變量 A 和 B,就會重複 ④ 和 ⑤ 這兩個步驟,Cache 并沒有起到緩存的效果,盡管變量 A 和 B 之間其實并沒有任何的關系,但是由于同時歸屬于一個 Cache Line ,這個 Cache Line 中的任意資料被修改後,都會互相影響,進而出現 ④ 和 ⑤ 這兩個步驟。

因而,這種由于多個線程同時讀寫同一個 Cache Line 的不同變量時,而導緻 CPU Cache 失效的現象稱為僞共享(*****False Sharing*****)。

避免僞共享的方法

因而,對于多個線程共享的熱點資料,即經常會修改的資料,應該避免這些資料恰好在同一個 Cache Line 中,否則就會出現為僞共享的問題。

接下來,看看在實際項目中是用什麼方式來避免僞共享的問題的。

在 Linux 核心中存在 __cacheline_aligned_in_smp 宏定義,是用于處理僞共享的問題。

什麼是僞共享?又該怎樣避免僞共享的問題?

從上面的宏定義,我們可以看到:

  • 假如在多核(MP)系統裡,該宏定義是 __cacheline_aligned,也就是 Cache Line 的大小;
  • 而假如在單核系統裡,該宏定義是空的;

因而,針對在同一個 Cache Line 中的共享的資料,假如在多核之間競争比較嚴重,為了防止僞共享現象的發生,可以采用上面的宏定義使得變量在 Cache Line 裡是對齊的。

舉個例子,有下面這個結構體:

什麼是僞共享?又該怎樣避免僞共享的問題?

結構體裡的兩個成員變量 a 和 b 在實體記憶體位址上是連續的,于是它們可能會位于同一個 Cache Line 中,如下圖:

什麼是僞共享?又該怎樣避免僞共享的問題?

是以,為了防止前面提到的 Cache 僞共享問題,我們可以使用上面詳情的宏定義,将 b 的位址設定為 Cache Line 對齊位址,如下:

什麼是僞共享?又該怎樣避免僞共享的問題?

這樣 a 和 b 變量就不會在同一個 Cache Line 中了,如下圖:

什麼是僞共享?又該怎樣避免僞共享的問題?

是以,避免 Cache 僞共享實際上是用空間換時間的思想,白費一部分 Cache 空間,進而換來性能的提升。

我們再來看一個應用層面的規避方案,有一個 Java 并發架構 Disruptor 使用「位元組填充 + 繼承」的方式,來避免僞共享的問題。

Disruptor 中有一個 RingBuffer 類會經常被多個線程使用,代碼如下:

什麼是僞共享?又該怎樣避免僞共享的問題?

你可能會覺得 RingBufferPad 類裡 7 個 long 類型的名字很奇怪,但事實上,它們盡管看起來毫無作用,但卻對性能的提升起到了至關重要的作用。

我們都知道,CPU Cache 從記憶體讀取資料的機關是 CPU Line,一般 64 位 CPU 的 CPU Line 的大小是 64 個位元組,一個 long 類型的資料是 8 個位元組,是以 CPU 一下會加載 8 個 long 類型的資料。

根據 JVM 對象繼承關系中父類成員和子類成員,記憶體位址是連續排列布局的,因而 RingBufferPad 中的 7 個 long 類型資料作為 Cache Line 前置填充,而 RingBuffer 中的 7 個 long 類型資料則作為 Cache Line 後置填充,這 14 個 long 變量沒有任何實際用途,更不會對它們進行讀寫操作。

什麼是僞共享?又該怎樣避免僞共享的問題?

另外,RingBufferFelds 裡面定義的這些變量都是 final 修飾的,意味着第一次加載之後不會再修改, 又因為「前後」各填充了 7 個不會被讀寫的 long 類型變量,是以無論怎樣加載 Cache Line,這整個 Cache Line 裡都沒有會發生更新操作的資料,于是隻需資料被頻繁地讀取通路,就自然沒有資料被換出 Cache 的可能,也因而不會産生僞共享的問題。

路是自己走出來的,而不是選出來的。