自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

字節(jié)面:什么是偽共享?

開(kāi)發(fā) 前端
周末的時(shí)候,有個(gè)讀者跟我說(shuō),面試字節(jié)的時(shí)候被問(wèn)到:「什么是偽共享?又該怎么避免偽共享的問(wèn)題?」這個(gè)其實(shí)是考察 CPU 緩存的問(wèn)題,我之前的圖解系統(tǒng)也有提到過(guò)。

 

本文轉(zhuǎn)載自微信公眾號(hào)「小林coding」,作者小林coding 。轉(zhuǎn)載本文請(qǐng)聯(lián)系小林coding公眾號(hào)。

大家好,我是小林。

周末的時(shí)候,有個(gè)讀者跟我說(shuō),面試字節(jié)的時(shí)候被問(wèn)到:「什么是偽共享?又該怎么避免偽共享的問(wèn)題?」

這個(gè)其實(shí)是考察 CPU 緩存的問(wèn)題,我之前的圖解系統(tǒng)也有提到過(guò)。

今天,我再跟大家講一下。

正文

CPU 如何讀寫數(shù)據(jù)的?

先來(lái)認(rèn)識(shí) CPU 的架構(gòu),只有理解了 CPU 的 架構(gòu),才能更好地理解 CPU 是如何讀寫數(shù)據(jù)的,對(duì)于現(xiàn)代 CPU 的架構(gòu)圖如下:

可以看到,一個(gè) CPU 里通常會(huì)有多個(gè) CPU 核心,比如上圖中的 1 號(hào)和 2 號(hào) CPU 核心,并且每個(gè) CPU 核心都有自己的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分為 dCache(數(shù)據(jù)緩存) 和 iCache(指令緩存),L3 Cache 則是多個(gè)核心共享的,這就是 CPU 典型的緩存層次。

上面提到的都是 CPU 內(nèi)部的 Cache,放眼外部的話,還會(huì)有內(nèi)存和硬盤,這些存儲(chǔ)設(shè)備共同構(gòu)成了金字塔存儲(chǔ)層次。如下圖所示:

從上圖也可以看到,從上往下,存儲(chǔ)設(shè)備的容量會(huì)越大,而訪問(wèn)速度會(huì)越慢。至于每個(gè)存儲(chǔ)設(shè)備的訪問(wèn)延時(shí),你可以看下圖的表格:

你可以看到, CPU 訪問(wèn) L1 Cache 速度比訪問(wèn)內(nèi)存快 100 倍,這就是為什么 CPU 里會(huì)有 L1~L3 Cache 的原因,目的就是把 Cache 作為 CPU 與內(nèi)存之間的緩存層,以減少對(duì)內(nèi)存的訪問(wèn)頻率。

CPU 從內(nèi)存中讀取數(shù)據(jù)到 Cache 的時(shí)候,并不是一個(gè)字節(jié)一個(gè)字節(jié)讀取,而是一塊一塊的方式來(lái)讀取數(shù)據(jù)的,這一塊一塊的數(shù)據(jù)被稱為 CPU Line(緩存行),所以 CPU Line 是 CPU 從內(nèi)存讀取數(shù)據(jù)到 Cache 的單位。

至于 CPU Line 大小,在 Linux 系統(tǒng)可以用下面的方式查看到,你可以看我服務(wù)器的 L1 Cache Line 大小是 64 字節(jié),也就意味著 L1 Cache 一次載入數(shù)據(jù)的大小是 64 字節(jié)。

那么對(duì)數(shù)組的加載, CPU 就會(huì)加載數(shù)組里面連續(xù)的多個(gè)數(shù)據(jù)到 Cache 里,因此我們應(yīng)該按照物理內(nèi)存地址分布的順序去訪問(wèn)元素,這樣訪問(wèn)數(shù)組元素的時(shí)候,Cache 命中率就會(huì)很高,于是就能減少?gòu)膬?nèi)存讀取數(shù)據(jù)的頻率, 從而可提高程序的性能。

但是,在我們不使用數(shù)組,而是使用單獨(dú)的變量的時(shí)候,則會(huì)有 Cache 偽共享的問(wèn)題,Cache 偽共享問(wèn)題上是一個(gè)性能殺手,我們應(yīng)該要規(guī)避它。

接下來(lái),就來(lái)看看 Cache 偽共享是什么?又如何避免這個(gè)問(wèn)題?

現(xiàn)在假設(shè)有一個(gè)雙核心的 CPU,這兩個(gè) CPU 核心并行運(yùn)行著兩個(gè)不同的線程,它們同時(shí)從內(nèi)存中讀取兩個(gè)不同的數(shù)據(jù),分別是類型為 long 的變量 A 和 B,這個(gè)兩個(gè)數(shù)據(jù)的地址在物理內(nèi)存上是連續(xù)的,如果 Cahce Line 的大小是 64 字節(jié),并且變量 A 在 Cahce Line 的開(kāi)頭位置,那么這兩個(gè)數(shù)據(jù)是位于同一個(gè) Cache Line 中,又因?yàn)?CPU Line 是 CPU 從內(nèi)存讀取數(shù)據(jù)到 Cache 的單位,所以這兩個(gè)數(shù)據(jù)會(huì)被同時(shí)讀入到了兩個(gè) CPU 核心中各自 Cache 中。

我們來(lái)思考一個(gè)問(wèn)題,如果這兩個(gè)不同核心的線程分別修改不同的數(shù)據(jù),比如 1 號(hào) CPU 核心的線程只修改了 變量 A,或 2 號(hào) CPU 核心的線程的線程只修改了變量 B,會(huì)發(fā)生什么呢?

分析偽共享的問(wèn)題

現(xiàn)在我們結(jié)合保證多核緩存一致的 MESI 協(xié)議,來(lái)說(shuō)明這一整個(gè)的過(guò)程,如果你還不知道 MESI 協(xié)議,你可以看我這篇文章「10 張圖打開(kāi) CPU 緩存一致性的大門」。

①. 最開(kāi)始變量 A 和 B 都還不在 Cache 里面,假設(shè) 1 號(hào)核心綁定了線程 A,2 號(hào)核心綁定了線程 B,線程 A 只會(huì)讀寫變量 A,線程 B 只會(huì)讀寫變量 B。

②. 1 號(hào)核心讀取變量 A,由于 CPU 從內(nèi)存讀取數(shù)據(jù)到 Cache 的單位是 Cache Line,也正好變量 A 和 變量 B 的數(shù)據(jù)歸屬于同一個(gè) Cache Line,所以 A 和 B 的數(shù)據(jù)都會(huì)被加載到 Cache,并將此 Cache Line 標(biāo)記為「獨(dú)占」?fàn)顟B(tài)。

③. 接著,2 號(hào)核心開(kāi)始從內(nèi)存里讀取變量 B,同樣的也是讀取 Cache Line 大小的數(shù)據(jù)到 Cache 中,此 Cache Line 中的數(shù)據(jù)也包含了變量 A 和 變量 B,此時(shí) 1 號(hào)和 2 號(hào)核心的 Cache Line 狀態(tài)變?yōu)椤腹蚕怼範(fàn)顟B(tài)。

④. 1 號(hào)核心需要修改變量 A,發(fā)現(xiàn)此 Cache Line 的狀態(tài)是「共享」?fàn)顟B(tài),所以先需要通過(guò)總線發(fā)送消息給 2 號(hào)核心,通知 2 號(hào)核心把 Cache 中對(duì)應(yīng)的 Cache Line 標(biāo)記為「已失效」?fàn)顟B(tài),然后 1 號(hào)核心對(duì)應(yīng)的 Cache Line 狀態(tài)變成「已修改」?fàn)顟B(tài),并且修改變量 A。

⑤. 之后,2 號(hào)核心需要修改變量 B,此時(shí) 2 號(hào)核心的 Cache 中對(duì)應(yīng)的 Cache Line 是已失效狀態(tài),另外由于 1 號(hào)核心的 Cache 也有此相同的數(shù)據(jù),且狀態(tài)為「已修改」?fàn)顟B(tài),所以要先把 1 號(hào)核心的 Cache 對(duì)應(yīng)的 Cache Line 寫回到內(nèi)存,然后 2 號(hào)核心再?gòu)膬?nèi)存讀取 Cache Line 大小的數(shù)據(jù)到 Cache 中,最后把變量 B 修改到 2 號(hào)核心的 Cache 中,并將狀態(tài)標(biāo)記為「已修改」?fàn)顟B(tài)。

所以,可以發(fā)現(xiàn)如果 1 號(hào)和 2 號(hào) CPU 核心這樣持續(xù)交替的分別修改變量 A 和 B,就會(huì)重復(fù) ④ 和 ⑤ 這兩個(gè)步驟,Cache 并沒(méi)有起到緩存的效果,雖然變量 A 和 B 之間其實(shí)并沒(méi)有任何的關(guān)系,但是因?yàn)橥瑫r(shí)歸屬于一個(gè) Cache Line ,這個(gè) Cache Line 中的任意數(shù)據(jù)被修改后,都會(huì)相互影響,從而出現(xiàn) ④ 和 ⑤ 這兩個(gè)步驟。

因此,這種因?yàn)槎鄠€(gè)線程同時(shí)讀寫同一個(gè) Cache Line 的不同變量時(shí),而導(dǎo)致 CPU Cache 失效的現(xiàn)象稱為偽共享(False Sharing)。

避免偽共享的方法

因此,對(duì)于多個(gè)線程共享的熱點(diǎn)數(shù)據(jù),即經(jīng)常會(huì)修改的數(shù)據(jù),應(yīng)該避免這些數(shù)據(jù)剛好在同一個(gè) Cache Line 中,否則就會(huì)出現(xiàn)為偽共享的問(wèn)題。

接下來(lái),看看在實(shí)際項(xiàng)目中是用什么方式來(lái)避免偽共享的問(wèn)題的。

在 Linux 內(nèi)核中存在 __cacheline_aligned_in_smp 宏定義,是用于解決偽共享的問(wèn)題。

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

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

因此,針對(duì)在同一個(gè) Cache Line 中的共享的數(shù)據(jù),如果在多核之間競(jìng)爭(zhēng)比較嚴(yán)重,為了防止偽共享現(xiàn)象的發(fā)生,可以采用上面的宏定義使得變量在 Cache Line 里是對(duì)齊的。

舉個(gè)例子,有下面這個(gè)結(jié)構(gòu)體:

結(jié)構(gòu)體里的兩個(gè)成員變量 a 和 b 在物理內(nèi)存地址上是連續(xù)的,于是它們可能會(huì)位于同一個(gè) Cache Line 中,如下圖:

所以,為了防止前面提到的 Cache 偽共享問(wèn)題,我們可以使用上面介紹的宏定義,將 b 的地址設(shè)置為 Cache Line 對(duì)齊地址,如下:

這樣 a 和 b 變量就不會(huì)在同一個(gè) Cache Line 中了,如下圖:

所以,避免 Cache 偽共享實(shí)際上是用空間換時(shí)間的思想,浪費(fèi)一部分 Cache 空間,從而換來(lái)性能的提升。

我們?cè)賮?lái)看一個(gè)應(yīng)用層面的規(guī)避方案,有一個(gè) Java 并發(fā)框架 Disruptor 使用「字節(jié)填充 + 繼承」的方式,來(lái)避免偽共享的問(wèn)題。

Disruptor 中有一個(gè) RingBuffer 類會(huì)經(jīng)常被多個(gè)線程使用,代碼如下:

你可能會(huì)覺(jué)得 RingBufferPad 類里 7 個(gè) long 類型的名字很奇怪,但事實(shí)上,它們雖然看起來(lái)毫無(wú)作用,但卻對(duì)性能的提升起到了至關(guān)重要的作用。

我們都知道,CPU Cache 從內(nèi)存讀取數(shù)據(jù)的單位是 CPU Line,一般 64 位 CPU 的 CPU Line 的大小是 64 個(gè)字節(jié),一個(gè) long 類型的數(shù)據(jù)是 8 個(gè)字節(jié),所以 CPU 一下會(huì)加載 8 個(gè) long 類型的數(shù)據(jù)。

根據(jù) JVM 對(duì)象繼承關(guān)系中父類成員和子類成員,內(nèi)存地址是連續(xù)排列布局的,因此 RingBufferPad 中的 7 個(gè) long 類型數(shù)據(jù)作為 Cache Line 前置填充,而 RingBuffer 中的 7 個(gè) long 類型數(shù)據(jù)則作為 Cache Line 后置填充,這 14 個(gè) long 變量沒(méi)有任何實(shí)際用途,更不會(huì)對(duì)它們進(jìn)行讀寫操作。

另外,RingBufferFelds 里面定義的這些變量都是 final 修飾的,意味著第一次加載之后不會(huì)再修改, 又由于「前后」各填充了 7 個(gè)不會(huì)被讀寫的 long 類型變量,所以無(wú)論怎么加載 Cache Line,這整個(gè) Cache Line 里都沒(méi)有會(huì)發(fā)生更新操作的數(shù)據(jù),于是只要數(shù)據(jù)被頻繁地讀取訪問(wèn),就自然沒(méi)有數(shù)據(jù)被換出 Cache 的可能,也因此不會(huì)產(chǎn)生偽共享的問(wèn)題。

 

責(zé)任編輯:武曉燕 來(lái)源: 小林coding
相關(guān)推薦

2021-03-01 11:53:15

面試偽共享CPU

2021-06-30 17:38:03

Trie 樹(shù)字符Java

2024-07-30 14:01:51

Java字節(jié)碼JVM?

2025-03-28 10:47:05

開(kāi)發(fā)注解Java

2024-08-30 08:59:15

2025-04-08 09:20:00

Sentinel限流微服務(wù)

2017-07-13 16:40:16

偽共享緩存行存儲(chǔ)

2022-12-12 08:39:09

CPUCache偽共享

2019-12-17 14:24:11

CPU緩存偽共享

2024-11-26 08:52:34

SQL優(yōu)化Kafka

2021-04-25 09:58:48

mmapJava面試

2021-03-17 15:54:32

IO零拷貝方式

2013-06-14 10:12:22

共享并行

2021-11-18 08:55:49

共享CPU內(nèi)存

2022-03-30 10:10:17

字節(jié)碼??臻g

2023-12-26 16:14:43

2015-04-23 09:34:51

Windows開(kāi)源

2017-08-23 13:21:31

2021-09-08 07:58:58

字節(jié)系統(tǒng)雙寫

2024-04-03 09:01:34

SpringTomcat容器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)