偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

優(yōu)化代碼性能:利用CPU緩存原理

開發(fā) 前端
在編寫程序時,充分考慮循環(huán)結(jié)構(gòu)對 CPU Cache 命中率的影響,并運用上述優(yōu)化方法,可以顯著提高程序的運行效率 。尤其是在處理大規(guī)模數(shù)據(jù)和對性能要求苛刻的應用場景中,循環(huán)優(yōu)化是提升程序性能的關鍵環(huán)節(jié)之一 。

在計算機的世界里,有一場如同龜兔賽跑般的速度較量,主角便是 CPU 和內(nèi)存 。龜兔賽跑的故事大家都耳熟能詳,兔子速度飛快,烏龜則慢吞吞的。在計算機中,CPU 就如同那敏捷的兔子,擁有超高的運算速度,能夠在極短的時間內(nèi)完成大量復雜的計算任務;而內(nèi)存則像是那步伐緩慢的烏龜,雖然它能夠存儲程序運行所需的數(shù)據(jù)和指令,但數(shù)據(jù)的讀取和寫入速度,與 CPU 相比,簡直是天壤之別。

現(xiàn)代 CPU 的運行頻率可以輕松達到數(shù) GHz,這意味著它每秒能夠執(zhí)行數(shù)十億次的操作。而內(nèi)存的訪問速度,雖然也在不斷提升,但與 CPU 的速度相比,仍然存在著巨大的差距。這種速度上的不匹配,就好比是讓一個短跑冠軍和一個普通人進行接力賽跑,每次短跑冠軍快速跑到終點后,都需要花費大量時間等待普通人慢悠悠地把接力棒送過來,這無疑會極大地影響整個系統(tǒng)的運行效率。

為了解決這個問題,計算機科學家們引入了一種名為 CPU Cache 的高速緩沖存儲器,它就像是在 CPU 和內(nèi)存之間搭建了一座 “高速橋梁”,讓 CPU 能夠更快地獲取數(shù)據(jù),從而提高計算機的整體性能 。接下來,就讓我們一起深入了解一下 CPU Cache 的奧秘吧。

一、CPU Cache概述

你可能會好奇為什么有了內(nèi)存,還需要 CPU Cache?根據(jù)摩爾定律,CPU 的訪問速度每 18 個月就會翻倍,相當于每年增長 60% 左右,內(nèi)存的速度當然也會不斷增長,但是增長的速度遠小于 CPU,平均每年只增長 7% 左右。于是,CPU 與內(nèi)存的訪問性能的差距不斷拉大。

到現(xiàn)在,一次內(nèi)存訪問所需時間是 200~300 多個時鐘周期,這意味著 CPU 和內(nèi)存的訪問速度已經(jīng)相差 200~300 多倍了。為了彌補CPU和內(nèi)存之間的性能差異,以便于能夠真實變得把CPU的性能提升利用起來,而不是讓它在那里空轉(zhuǎn),我們在現(xiàn)代CPU中引入了高速緩存。

從CPU Cache被加入到現(xiàn)有的CPU里開始,內(nèi)存中的指令、數(shù)據(jù),會被加載到L1-L3 Cache中,而不是直接從CPU訪問內(nèi)存中取拿。CPU從內(nèi)存讀取數(shù)據(jù)到CPU Cache的過程中,是一小塊一小塊來讀取數(shù)據(jù)的,而不是按照單個數(shù)組元素來讀取數(shù)據(jù)的。這樣一小塊一小塊的數(shù)據(jù),在CPU Cache里面,叫做Cache Line(緩存塊)

在我們?nèi)粘J褂玫?Intel 服務器或者 PC 里,Cache Line 的大小通常是 64 字節(jié)。

1.1Cache 的定義與角色

CPU Cache,即高速緩沖存儲器,是位于 CPU 和內(nèi)存之間的臨時存儲器 。它就像是一個數(shù)據(jù) “中轉(zhuǎn)站”,主要作用是為了加速 CPU 讀取數(shù)據(jù)的速度 。由于 CPU 的運算速度極快,而內(nèi)存的讀寫速度相對較慢,這就好比一個短跑冠軍和一個慢跑者,兩者的速度差距明顯。

如果 CPU 直接從內(nèi)存中讀取數(shù)據(jù),就需要花費大量時間等待,這無疑會降低整個計算機系統(tǒng)的運行效率 。而 CPU Cache 的出現(xiàn),就很好地解決了這個問題。它的速度比內(nèi)存快很多,能夠提前將 CPU 可能需要的數(shù)據(jù)從內(nèi)存中讀取出來并存儲起來,當 CPU 需要數(shù)據(jù)時,首先會在 Cache 中查找,如果找到了,就可以直接從 Cache 中快速獲取,大大節(jié)省了讀取數(shù)據(jù)的時間,提高了 CPU 的工作效率 。

1.2Cache 的層級結(jié)構(gòu)

隨著科技發(fā)展,熱點數(shù)據(jù)的體積越來越大,單純的增加一級緩存大小的性價比已經(jīng)很低了;二級緩存就是一級緩存的緩沖器:一級緩存制造成本很高因此它的容量有限,二級緩存的作用就是存儲那些CPU處理時需要用到、一級緩存又無法存儲的數(shù)據(jù)。

CPU Cache 通常分為三級,分別是 L1 Cache(一級緩存)、L2 Cache(二級緩存)和 L3 Cache(三級緩存) 。這三級緩存就像是一個金字塔結(jié)構(gòu),從 L1 到 L3,速度逐漸變慢,容量逐漸增大 。

L1 Cache 是離 CPU 核心最近的緩存,它的速度最快,幾乎可以與 CPU 的頻率同步運行 。L1 Cache 又可以細分為數(shù)據(jù)緩存(L1 D - Cache)和指令緩存(L1 I - Cache),分別用于存儲數(shù)據(jù)和指令 。

數(shù)據(jù)緩存就像是一個小型的數(shù)據(jù)倉庫,專門存放 CPU 在運算過程中需要處理的數(shù)據(jù);指令緩存則像是一個指令庫,存儲著 CPU 執(zhí)行運算時所需要的指令 。由于 L1 Cache 與CPU 核心緊密相連,訪問速度極快,但其容量相對較小,一般只有幾十 KB 到幾百 KB 。例如,英特爾酷睿 i7 - 13700K 處理器,其每個核心的 L1 數(shù)據(jù)緩存為 32KB,L1指令緩存也為 32KB 。

L2 Cache 位于 L1 Cache 之后,速度比 L1 Cache 稍慢,但容量比 L1 Cache 大 。它的作用是作為 L1 Cache的補充,當 L1 Cache 中沒有找到 CPU 需要的數(shù)據(jù)或指令時,CPU 就會到L2 Cache 中查找 。L2 Cache 的容量一般在幾百 KB 到幾 MB 之間 。以剛才提到的酷睿i7 - 13700K 為例,其每個核心的L2緩存為 1MB 。

L3 Cache 是三級緩存中速度最慢但容量最大的緩存,它通常是多個 CPU 核心共享的 。當 L1 和 L2 Cache 都沒有命中時,CPU 會訪問 L3 Cache 。L3 Cache 的存在可以進一步提高數(shù)據(jù)的命中率,減少 CPU 訪問內(nèi)存的次數(shù) 。它的容量一般在幾 MB 到幾十 MB 之間 。酷睿 i7 - 13700K 的 L3 緩存為 30MB 。

在實際工作中,CPU 按照 L1 Cache、L2 Cache、L3 Cache 的順序依次查找數(shù)據(jù) 。如果在某一級緩存中找到了所需的數(shù)據(jù),就稱為緩存命中;如果在三級緩存中都沒有找到,才會去內(nèi)存中查找,這就是緩存未命中 。緩存命中的概率越高,CPU 獲取數(shù)據(jù)的速度就越快,計算機的性能也就越好 。

二、CPU Cache核心原理

2.1局部性原理

CPU Cache 能夠提高數(shù)據(jù)訪問性能,背后依賴的是局部性原理 。局部性原理又分為時間局部性和空間局部性 。

時間局部性,是指如果一個數(shù)據(jù)項在某個時刻被訪問,那么在不久的將來它很可能再次被訪問 。就好比我們看電視劇時,喜歡反復觀看精彩的片段。在程序中,循環(huán)結(jié)構(gòu)就是時間局部性的典型體現(xiàn) 。例如下面這段簡單的 C 語言代碼:

int sum = 0;
int arr[100] = {1, 2, 3,..., 100};
for (int i = 0; i < 100; i++) {
    sum += arr[i];
}

在這個循環(huán)中,變量sum會被多次訪問,每次循環(huán)都要讀取和更新它的值 。根據(jù)時間局部性原理,CPU Cache 會將sum的值緩存起來,這樣在后續(xù)的循環(huán)中,CPU 就可以直接從 Cache 中快速獲取sum的值,而不需要每次都從內(nèi)存中讀取 ,大大提高了訪問效率 。

空間局部性,是指如果一個數(shù)據(jù)項被訪問,那么與其相鄰的數(shù)據(jù)項很可能在不久的將來也被訪問 。這就像我們在書架上找書,找到一本后,往往會順便看看它旁邊的書 。在計算機中,內(nèi)存中的數(shù)據(jù)通常是按順序存儲的 。比如數(shù)組,數(shù)組元素在內(nèi)存中是連續(xù)存放的 。當 CPU 訪問數(shù)組中的一個元素時,根據(jù)空間局部性原理,它很可能在接下來的操作中訪問該元素附近的其他元素 。

還是以上面的代碼為例,當 CPU 訪問arr[i]時,Cache 會把arr[i]以及它附近的一些元素(比如arr[i - 1]、arr[i + 1]等,具體取決于 Cache Line 的大小,后面會詳細介紹 Cache Line)一起緩存起來 。這樣,當 CPU 訪問下一個元素arr[i + 1]時,就有可能直接從 Cache 中獲取,而無需再次訪問內(nèi)存 。

CPU Cache 正是利用了這兩種局部性原理,提前將可能被訪問的數(shù)據(jù)從內(nèi)存中讀取到 Cache 中 。當 CPU 需要數(shù)據(jù)時,首先在 Cache 中查找,如果命中(即找到所需數(shù)據(jù)),就可以快速獲取數(shù)據(jù),大大縮短了數(shù)據(jù)訪問時間 。如果未命中,才會去內(nèi)存中讀取數(shù)據(jù),并將讀取到的數(shù)據(jù)及其相鄰的數(shù)據(jù)塊存入 Cache,以便后續(xù)訪問 。通過這種方式,CPU Cache 有效地提高了數(shù)據(jù)訪問性能,減少了 CPU 等待數(shù)據(jù)的時間,從而提升了整個計算機系統(tǒng)的運行效率 。

2.2緩存命中與未命中

在 CPU 訪問數(shù)據(jù)的過程中,緩存命中和未命中是兩個非常關鍵的概念,它們對 CPU 的數(shù)據(jù)讀取流程有著重要的影響 。

當 CPU 需要讀取某個數(shù)據(jù)時,它會首先在 Cache 中查找該數(shù)據(jù) 。如果該數(shù)據(jù)正好存在于 Cache 中,這就稱為緩存命中 。緩存命中是一種非常理想的情況,因為 Cache 的速度比內(nèi)存快很多,CPU 可以直接從 Cache 中快速獲取數(shù)據(jù),幾乎不需要等待 。這就好比你在自己的書架上找一本書,一下子就找到了,馬上就可以開始閱讀 。

在緩存命中的情況下,CPU 的數(shù)據(jù)讀取流程非常簡單高效,直接從 Cache 中讀取數(shù)據(jù)并進行后續(xù)的運算操作,大大提高了 CPU 的工作效率 。例如,在一個頻繁訪問數(shù)組元素的程序中,如果數(shù)組元素被緩存到 Cache 中,當 CPU 再次訪問這些元素時,就會發(fā)生緩存命中,CPU 能夠迅速獲取數(shù)據(jù),使得程序能夠快速運行 。

然而,如果 CPU 在 Cache 中沒有找到所需的數(shù)據(jù),這就是緩存未命中 。緩存未命中的情況相對復雜,對 CPU 的性能影響也較大 。當緩存未命中時,CPU 不得不從速度較慢的內(nèi)存中讀取數(shù)據(jù) 。這就像你在自己的書架上沒找到想要的書,只能去圖書館的書庫中尋找,這無疑會花費更多的時間 。

在從內(nèi)存讀取數(shù)據(jù)的過程中,CPU 需要等待內(nèi)存返回數(shù)據(jù),這個等待時間可能會包含多個時鐘周期 。而且,在從內(nèi)存讀取數(shù)據(jù)的同時,為了提高后續(xù)數(shù)據(jù)訪問的命中率,CPU 會將讀取到的數(shù)據(jù)以及該數(shù)據(jù)周圍的一部分數(shù)據(jù)(以 Cache Line 為單位,后面會詳細介紹)存入 Cache 中 。

如果此時 Cache 已滿,還需要采用一定的替換算法(如最近最少使用 LRU 算法等),將 Cache 中不太常用的數(shù)據(jù)替換出去,為新的數(shù)據(jù)騰出空間 。例如,在一個處理大數(shù)據(jù)集的程序中,如果數(shù)據(jù)量超過了 Cache 的容量,就很容易出現(xiàn)緩存未命中的情況 。每次緩存未命中都需要 CPU 從內(nèi)存讀取數(shù)據(jù),這會導致程序的運行速度明顯下降,因為內(nèi)存訪問的延遲相對較高 。

緩存命中率是衡量 Cache 性能的重要指標,它表示緩存命中次數(shù)在總訪問次數(shù)中所占的比例 。緩存命中率越高,說明 CPU 從 Cache 中獲取數(shù)據(jù)的次數(shù)越多,也就意味著 CPU 等待數(shù)據(jù)的時間越短,計算機系統(tǒng)的性能也就越好 。因此,在計算機系統(tǒng)設計和程序優(yōu)化中,提高緩存命中率是一個重要的目標 。通過合理地利用局部性原理、優(yōu)化數(shù)據(jù)訪問模式以及選擇合適的 Cache 大小和替換算法等方法,可以有效地提高緩存命中率,減少緩存未命中的次數(shù),從而提升計算機系統(tǒng)的整體性能 。

2.3Cache Line

Cache Line 是 CPU Cache 中的一個重要概念,它是 CPU 與內(nèi)存之間進行數(shù)據(jù)傳輸?shù)淖钚挝?。簡單來說,當 CPU 需要從內(nèi)存讀取數(shù)據(jù)到 Cache 時,并不是以單個字節(jié)為單位進行讀取,而是一次性讀取一個固定大小的數(shù)據(jù)塊,這個數(shù)據(jù)塊就是一個 Cache Line 。

Cache Line 的大小通常是固定的,常見的 Cache Line 大小有 32 字節(jié)、64 字節(jié)等 。不同的 CPU 架構(gòu)可能會有不同的 Cache Line 大小 。例如,在許多現(xiàn)代 x86 架構(gòu)的 CPU 中,Cache Line 的大小一般為 64 字節(jié) 。Cache Line 的存在主要是為了利用空間局部性原理,提高數(shù)據(jù)讀取的效率 。當 CPU 訪問某個內(nèi)存地址時,雖然它只需要該地址處的一個數(shù)據(jù),但由于空間局部性,與該地址相鄰的數(shù)據(jù)很可能也會被訪問 。因此,將該地址所在的一整段數(shù)據(jù)(即一個 Cache Line)都讀取到 Cache 中,可以減少后續(xù)數(shù)據(jù)訪問時的緩存未命中次數(shù) 。

舉個例子,假設有一個包含 100 個整數(shù)的數(shù)組,每個整數(shù)占 4 字節(jié),數(shù)組在內(nèi)存中是連續(xù)存儲的 。如果 Cache Line 大小為 64 字節(jié),那么一個 Cache Line 可以容納 16 個整數(shù)(64 字節(jié) / 4 字節(jié) = 16) 。當 CPU 訪問數(shù)組中的第 1 個元素時,內(nèi)存會將包含第 1 個元素以及其后面 15 個元素的這 64 字節(jié)數(shù)據(jù)作為一個 Cache Line 讀取到 Cache 中 。這樣,當 CPU 接下來訪問數(shù)組中的第 2 個到第 16 個元素時,就可以直接從 Cache 中獲取,而不需要再次訪問內(nèi)存,大大提高了數(shù)據(jù)訪問的效率 。

Cache Line 的大小對程序性能有著重要的影響 。如果 Cache Line 過小,雖然每次讀取的數(shù)據(jù)量少,讀取速度可能會快一些,但由于不能充分利用空間局部性,可能會導致緩存未命中次數(shù)增加;如果 Cache Line 過大,雖然能更好地利用空間局部性,減少緩存未命中次數(shù),但每次讀取的數(shù)據(jù)量過多,可能會導致 Cache 的利用率降低,而且讀取時間也可能會變長 。因此,在設計 CPU Cache 時,需要綜合考慮各種因素,選擇合適的 Cache Line 大小,以達到最佳的性能 。

在編寫程序時,了解 Cache Line 的概念也有助于優(yōu)化程序性能 。例如,在處理數(shù)組時,可以通過合理地組織數(shù)據(jù)結(jié)構(gòu)和訪問順序,使得數(shù)據(jù)訪問能夠更好地利用 Cache Line,提高緩存命中率 。比如,按行訪問二維數(shù)組通常比按列訪問更能利用 Cache Line,因為二維數(shù)組在內(nèi)存中是按行存儲的,按行訪問時相鄰元素更有可能在同一個 Cache Line 中 。

三、CPU Cache的數(shù)據(jù)寫入策略

當 CPU 對 Cache 進行寫操作時,為了確保 Cache 和內(nèi)存之間的數(shù)據(jù)一致性以及提高系統(tǒng)性能,會采用不同的數(shù)據(jù)寫入策略,其中最常見的是直寫(Write Through)和寫回(Write Back)策略 。

3.1直寫(Write Through)

直寫,也被稱為寫直通或?qū)懘┩?。其操作邏輯非常直觀:當 CPU 要寫入數(shù)據(jù)時,數(shù)據(jù)會同時被寫入 Cache 和內(nèi)存 。也就是說,只要有寫操作發(fā)生,Cache 和內(nèi)存中的數(shù)據(jù)都會立即被更新 。這就好比你在兩個筆記本上同時記錄同一件事情,一個筆記本是 Cache,另一個筆記本是內(nèi)存 。

直寫策略的優(yōu)點是簡單易懂,并且能夠始終保證 Cache 和內(nèi)存中的數(shù)據(jù)一致性 。因為每次寫操作都同步更新了內(nèi)存,所以在任何時刻,內(nèi)存中的數(shù)據(jù)都是最新的 。這種一致性在一些對數(shù)據(jù)一致性要求極高的場景中非常重要,比如數(shù)據(jù)庫系統(tǒng)中的關鍵數(shù)據(jù)存儲 。在數(shù)據(jù)庫事務處理中,需要確保數(shù)據(jù)的持久性和一致性,直寫策略可以保證每次數(shù)據(jù)修改都能及時保存到內(nèi)存中,避免了數(shù)據(jù)丟失或不一致的問題 。

然而,直寫策略也存在明顯的缺點 。由于每次寫操作都要訪問內(nèi)存,而內(nèi)存的訪問速度相對較慢,這就導致了寫操作的性能較低 。每次寫操作都需要等待內(nèi)存寫入完成,這會增加 CPU 的等待時間,降低了系統(tǒng)的整體效率 。而且,頻繁地訪問內(nèi)存還會增加總線的負載,因為每次寫操作都需要通過總線與內(nèi)存進行數(shù)據(jù)傳輸,可能會導致總線成為系統(tǒng)性能的瓶頸 。例如,在一個頻繁進行數(shù)據(jù)寫入的實時監(jiān)控系統(tǒng)中,大量的寫操作會使 CPU 花費大量時間等待內(nèi)存寫入,從而影響系統(tǒng)對其他任務的響應速度 。

3.2寫回(Write Back)

寫回策略的工作機制與直寫策略有很大不同 。在寫回策略中,當 CPU 進行寫操作時,數(shù)據(jù)首先被寫入 Cache,并不會立即寫入內(nèi)存 。只有當被修改的 Cache Line(緩存行,是 Cache 與內(nèi)存之間數(shù)據(jù)交換的最小單位)要被替換出 Cache 時(比如 Cache 已滿,需要騰出空間來存放新的數(shù)據(jù)),才會將其寫回到內(nèi)存中 。

為了實現(xiàn)這種延遲寫入的機制,每個 Cache Line 都有一個臟標記位(Dirty Bit) 。當數(shù)據(jù)被寫入 Cache 時,臟標記位會被設置,表示該 Cache Line 中的數(shù)據(jù)已經(jīng)被修改,與內(nèi)存中的數(shù)據(jù)不一致 。當 Cache Line 需要被替換時,系統(tǒng)會檢查其臟標記位 。如果臟標記位被設置,說明該 Cache Line 中的數(shù)據(jù)是最新的,需要先將其寫回到內(nèi)存中,然后再進行替換操作;如果臟標記位未被設置,說明該 Cache Line 中的數(shù)據(jù)與內(nèi)存中的數(shù)據(jù)一致,直接進行替換即可 。

寫回策略的主要優(yōu)點是性能較高 。由于大部分寫操作只需要在 Cache 中進行,不需要頻繁地訪問內(nèi)存,減少了內(nèi)存訪問的次數(shù),從而提高了系統(tǒng)的整體性能 。這種策略尤其適用于那些存在大量寫操作的應用場景,比如圖形渲染、視頻編碼等 。在圖形渲染過程中,GPU 會對大量的圖形數(shù)據(jù)進行處理和修改,采用寫回策略可以減少數(shù)據(jù)寫入內(nèi)存的次數(shù),加快渲染速度 。

與直寫策略相比,寫回策略在性能上有明顯的優(yōu)勢 。直寫策略每次寫操作都要訪問內(nèi)存,而寫回策略只有在 Cache Line 被替換時才會訪問內(nèi)存,大大減少了內(nèi)存訪問的頻率 。但是,寫回策略的實現(xiàn)復雜度較高,因為它需要額外維護臟標記位,并且在 Cache Line 替換時需要進行更多的判斷和操作 。

同時,由于數(shù)據(jù)不是立即寫入內(nèi)存,在系統(tǒng)突然斷電或崩潰的情況下,可能會導致 Cache 中已修改但未寫回內(nèi)存的數(shù)據(jù)丟失,從而出現(xiàn)數(shù)據(jù)不一致的問題 。為了應對這種風險,一些系統(tǒng)會采用寫緩沖區(qū)(Write Buffer)或日志記錄(Logging)等機制來保證數(shù)據(jù)的一致性 。寫緩沖區(qū)可以在數(shù)據(jù)寫回內(nèi)存之前臨時存儲數(shù)據(jù),即使系統(tǒng)崩潰,也可以從寫緩沖區(qū)中恢復數(shù)據(jù);日志記錄則可以記錄數(shù)據(jù)的修改操作,以便在系統(tǒng)恢復時進行數(shù)據(jù)的一致性恢復 。

四、多核時代的挑戰(zhàn):緩存一致性問題

當 CPU 對 Cache 進行寫操作時,為了確保 Cache 和內(nèi)存之間的數(shù)據(jù)一致性以及提高系統(tǒng)性能,會采用不同的數(shù)據(jù)寫入策略,其中最常見的是直寫(Write Through)和寫回(Write Back)策略 。

4.1緩存一致性問題的產(chǎn)生

在多核 CPU 的時代,每個 CPU 核心都擁有自己獨立的緩存,這雖然進一步提高了數(shù)據(jù)訪問的速度,但也帶來了一個新的棘手問題 —— 緩存一致性問題 。

想象一下,有一個多核心的 CPU,其中核心 A 和核心 B 都需要訪問內(nèi)存中的同一個數(shù)據(jù) X 。一開始,數(shù)據(jù) X 被加載到核心 A 和核心 B 各自的緩存中 。當核心 A 對緩存中的數(shù)據(jù) X 進行修改時,此時核心 A 緩存中的數(shù)據(jù) X 已經(jīng)更新,但核心 B 緩存中的數(shù)據(jù) X 仍然是舊的 。如果在這個時候,核心 B 讀取自己緩存中的數(shù)據(jù) X,就會得到一個錯誤的、過時的值,這就導致了數(shù)據(jù)不一致的情況 。

以一個簡單的銀行轉(zhuǎn)賬程序為例,假設有兩個線程分別在不同的 CPU 核心上執(zhí)行轉(zhuǎn)賬操作 。這兩個線程都需要讀取賬戶余額,然后進行相應的加減操作 。如果存在緩存一致性問題,就可能出現(xiàn)這樣的情況:第一個線程讀取了賬戶余額為 1000 元,然后在自己的緩存中進行了減 100 元的操作,但還沒有將更新后的數(shù)據(jù)寫回內(nèi)存 。

此時,第二個線程從自己的緩存中讀取賬戶余額,由于其緩存中的數(shù)據(jù)沒有更新,仍然是 1000 元,然后也進行了減 100 元的操作 。最后,兩個線程都將各自緩存中的數(shù)據(jù)寫回內(nèi)存,結(jié)果賬戶余額就變成了 800 元,而實際上應該是 900 元 。這種數(shù)據(jù)不一致的情況會對程序的正確性產(chǎn)生嚴重影響,尤其是在涉及到金融、數(shù)據(jù)庫等對數(shù)據(jù)準確性要求極高的領域 。

4.2解決緩存一致性的方案

要解決緩存一致性問題,首先要解決的是多個 CPU 核心之間的數(shù)據(jù)傳播問題。最常見的一種解決方案呢,叫作總線嗅探

⑴總線嗅探Bus Snooping

總線嗅探是一種解決緩存一致性問題的基本機制 。在這種機制下,每個 CPU 核心都通過總線與內(nèi)存相連,并且每個核心都可以 “嗅探” 總線上的通信 。當一個 CPU 核心對自己緩存中的數(shù)據(jù)進行寫操作時,它會向總線發(fā)送一個寫請求,這個請求包含了被修改數(shù)據(jù)的地址等信息 。

總線上的其他 CPU 核心會監(jiān)聽這個請求,當它們發(fā)現(xiàn)請求中的地址與自己緩存中某數(shù)據(jù)的地址相同時,就會根據(jù)請求的類型(例如是寫操作還是使緩存失效的操作),對自己緩存中的數(shù)據(jù)進行相應的處理 。如果是寫操作,其他核心可能會選擇將自己緩存中的該數(shù)據(jù)標記為無效,這樣下次訪問時就會從內(nèi)存中重新讀取最新的數(shù)據(jù);如果是使緩存失效的操作,直接將對應緩存數(shù)據(jù)標記為無效即可 。

總線嗅探的優(yōu)點是實現(xiàn)相對簡單,不需要復雜的目錄結(jié)構(gòu)來記錄緩存狀態(tài) 。它能夠快速地檢測到其他核心對共享數(shù)據(jù)的修改,從而及時更新自己的緩存,保證數(shù)據(jù)的一致性 。然而,它也存在一些明顯的缺點 。隨著 CPU 核心數(shù)量的增加,總線上的通信量會大幅增加,因為每次寫操作都要通過總線廣播通知其他核心,這會導致總線帶寬成為瓶頸,降低系統(tǒng)的整體性能 。而且,總線嗅探機制在處理復雜的多處理器系統(tǒng)時,可能會出現(xiàn)一些一致性問題,例如在多個核心同時進行讀寫操作時,可能會出現(xiàn)數(shù)據(jù)更新順序不一致的情況 。

⑵MESI 協(xié)議

MESI 協(xié)議是一種廣泛應用的緩存一致性協(xié)議,它是對總線嗅探機制的進一步優(yōu)化和完善 。MESI 代表了緩存行的四種狀態(tài):Modified(已修改)、Exclusive(獨占)、Shared(共享)和 Invalid(無效) 。

  • Modified(已修改,M):當一個 CPU 核心對緩存中的數(shù)據(jù)進行修改后,該數(shù)據(jù)所在的緩存行狀態(tài)變?yōu)?M 。此時,緩存中的數(shù)據(jù)與內(nèi)存中的數(shù)據(jù)不一致,并且只有這個核心擁有該數(shù)據(jù)的最新副本 。在該緩存行被寫回內(nèi)存之前,其他核心如果要訪問這個數(shù)據(jù),必須先等待該核心將數(shù)據(jù)寫回內(nèi)存 。
  • Exclusive(獨占,E):當一個 CPU 核心從內(nèi)存中讀取數(shù)據(jù)到緩存時,如果其他核心的緩存中沒有該數(shù)據(jù)的副本,那么該緩存行處于 E 狀態(tài) 。此時,緩存中的數(shù)據(jù)與內(nèi)存中的數(shù)據(jù)一致,并且這個核心獨占該數(shù)據(jù) 。如果有其他核心也讀取這個數(shù)據(jù),該緩存行狀態(tài)會變?yōu)?S 。
  • Shared(共享,S):當多個 CPU 核心的緩存中都存在同一個數(shù)據(jù)的副本時,這些緩存行都處于 S 狀態(tài) 。此時,緩存中的數(shù)據(jù)與內(nèi)存中的數(shù)據(jù)一致,各個核心都可以同時讀取該數(shù)據(jù),但如果有一個核心要對數(shù)據(jù)進行寫操作,就需要先將其他核心緩存中該數(shù)據(jù)的副本失效,然后才能進行修改,修改后緩存行狀態(tài)變?yōu)?M 。
  • Invalid(無效,I):當一個緩存行中的數(shù)據(jù)被其他核心修改,或者該緩存行被替換出緩存時,它的狀態(tài)就變?yōu)?I 。處于 I 狀態(tài)的緩存行中的數(shù)據(jù)是無效的,不能被使用 。

MESI協(xié)議中的運行機制

假設有三個CPU A、B、C,對應三個緩存分別是cache a、b、 c。在主內(nèi)存中定義了x的引用值為0。

圖片圖片

①單核讀取,那么執(zhí)行流程是:CPU A發(fā)出了一條指令,從主內(nèi)存中讀取x。從主內(nèi)存通過bus讀取到緩存中(遠端讀取Remote read),這是該Cache line修改為E狀態(tài)(獨享)。

圖片圖片

②雙核讀取,那么執(zhí)行流程是:

  • CPU A發(fā)出了一條指令,從主內(nèi)存中讀取x。
  • CPU A從主內(nèi)存通過bus讀取到 cache a中并將該cache line 設置為E狀態(tài)。
  • CPU B發(fā)出了一條指令,從主內(nèi)存中讀取x。
  • CPU B試圖從主內(nèi)存中讀取x時,CPU A檢測到了地址沖突。這時CPU A對相關數(shù)據(jù)做出響應。此時x 存儲于cache a和cache b中,x在chche a和cache b中都被設置為S狀態(tài)(共享)。

圖片圖片

③修改數(shù)據(jù),那么執(zhí)行流程是:

  • CPU A 計算完成后發(fā)指令需要修改x.
  • CPU A 將x設置為M狀態(tài)(修改)并通知緩存了x的CPU B, CPU B將本地cache b中的x設置為I狀態(tài)(無效)
  • CPU A 對x進行賦值。

圖片圖片

④同步數(shù)據(jù),那么執(zhí)行流程是:

  • CPU B 發(fā)出了要讀取x的指令。
  • CPU B 通知CPU A,CPU A將修改后的數(shù)據(jù)同步到主內(nèi)存時cache a 修改為E(獨享)
  • CPU A同步CPU B的x,將cache a和同步后cache b中的x設置為S狀態(tài)(共享)。

圖片圖片

MESI 協(xié)議通過狀態(tài)轉(zhuǎn)換機制來保證緩存一致性 。例如,當一個處于 S 狀態(tài)的緩存行接收到一個寫請求時,擁有該緩存行的核心會向總線發(fā)送一個 Invalidate 消息,通知其他核心將它們緩存中該數(shù)據(jù)的副本失效 。其他核心收到這個消息后,將自己緩存中對應的緩存行狀態(tài)變?yōu)?I,并返回一個 Invalidate Acknowledge 消息 。當發(fā)起寫請求的核心收到所有其他核心的確認消息后,它就可以將自己緩存中的數(shù)據(jù)修改,并將緩存行狀態(tài)變?yōu)?M 。

MESI 協(xié)議的優(yōu)勢在于它能夠有效地減少總線帶寬的占用 。通過狀態(tài)轉(zhuǎn)換機制,只有在必要時才會進行總線通信,例如當一個核心要修改共享數(shù)據(jù)時才會通知其他核心使緩存失效,而不是像總線嗅探那樣每次寫操作都進行廣播 。這大大提高了系統(tǒng)在多核環(huán)境下的性能和可擴展性 。同時,MESI 協(xié)議還能很好地保證數(shù)據(jù)的一致性,確保各個核心在任何時刻都能訪問到正確的數(shù)據(jù) 。然而,MESI 協(xié)議的實現(xiàn)相對復雜,需要額外的硬件支持來維護緩存行的狀態(tài)和進行狀態(tài)轉(zhuǎn)換 。而且,在一些極端情況下,例如大量核心同時進行讀寫操作時,MESI 協(xié)議的性能也會受到一定的影響 。

五、如何利用 CPU Cache 優(yōu)化代碼

5.1數(shù)據(jù)對齊

在計算機中,數(shù)據(jù)對齊是指將數(shù)據(jù)存儲在內(nèi)存地址是其自身大小整數(shù)倍的位置上 。比如,一個 4 字節(jié)的整數(shù)(如int類型),應該存儲在地址能被 4 整除的地方;一個 8 字節(jié)的雙精度浮點數(shù)(如double類型),應該存儲在地址能被 8 整除的位置 。

數(shù)據(jù)對齊對 CPU Cache 訪問效率有著重要的影響 ?,F(xiàn)代 CPU 在訪問內(nèi)存時,是以 Cache Line 為單位進行數(shù)據(jù)讀取和寫入的,常見的 Cache Line 大小為 64 字節(jié) 。當數(shù)據(jù)對齊時,它們更有可能完整地位于同一個 Cache Line 內(nèi) 。例如,有一個包含多個int類型元素的數(shù)組,每個int占 4 字節(jié) 。如果數(shù)組元素是對齊存儲的,那么連續(xù)的 16 個int元素就可以剛好存放在一個 64 字節(jié)的 Cache Line 中 。當 CPU 訪問這個數(shù)組的某個元素時,就可以一次性將包含該元素以及相鄰 15 個元素的 Cache Line 讀入 Cache,后續(xù)訪問相鄰元素時就很可能直接從 Cache 中命中,大大提高了訪問效率 。

相反,如果數(shù)據(jù)沒有對齊,就可能出現(xiàn)一個數(shù)據(jù)跨越兩個 Cache Line 的情況 。比如,一個int類型的數(shù)據(jù)本該存儲在地址為 4 的倍數(shù)的位置,但卻存儲在了一個非 4 倍數(shù)的地址上,這就可能導致它的一部分在一個 Cache Line 中,另一部分在另一個 Cache Line 中 。當 CPU 訪問這個數(shù)據(jù)時,就需要讀取兩個 Cache Line,增加了內(nèi)存訪問次數(shù)和時間,降低了 Cache 命中率和訪問效率 。

下面通過一個簡單的 C 語言代碼示例來展示數(shù)據(jù)對齊前后的性能差異 。我們定義一個結(jié)構(gòu)體,分別測試對齊和未對齊情況下對結(jié)構(gòu)體數(shù)組的訪問速度 。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

// 未對齊的結(jié)構(gòu)體
struct UnalignedStruct {
    char a;
    int b;
    char c;
};

// 對齊的結(jié)構(gòu)體,使用__attribute__((aligned(4)))確保結(jié)構(gòu)體按4字節(jié)對齊
struct __attribute__((aligned(4))) AlignedStruct {
    char a;
    int b;
    char c;
};

// 測試函數(shù),計算對結(jié)構(gòu)體數(shù)組的訪問時間
void testAccessTime(void *arr, size_t numElements, size_t structSize) {
    clock_t start, end;
    double cpu_time_used;
    int i;

    start = clock();
    for (i = 0; i < numElements; i++) {
        // 模擬對結(jié)構(gòu)體成員的訪問
        char *ptr = (char *)arr + i * structSize;
        char temp = *((char *)ptr);
        temp = *((int *)(ptr + 1));
        temp = *((char *)(ptr + 5));
    }
    end = clock();

    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Access time: %f seconds\n", cpu_time_used);
}

int main() {
    size_t numElements = 1000000;
    struct UnalignedStruct *unalignedArr = (struct UnalignedStruct *)malloc(numElements * sizeof(struct UnalignedStruct));
    struct AlignedStruct *alignedArr = (struct AlignedStruct *)malloc(numElements * sizeof(struct AlignedStruct));

    if (unalignedArr == NULL || alignedArr == NULL) {
        perror("malloc failed");
        return 1;
    }

    printf("Testing unaligned struct:\n");
    testAccessTime(unalignedArr, numElements, sizeof(struct UnalignedStruct));

    printf("Testing aligned struct:\n");
    testAccessTime(alignedArr, numElements, sizeof(struct AlignedStruct));

    free(unalignedArr);
    free(alignedArr);

    return 0;
}

在這個示例中,UnalignedStruct是未對齊的結(jié)構(gòu)體,AlignedStruct是通過__attribute__((aligned(4)))進行了 4 字節(jié)對齊的結(jié)構(gòu)體 。testAccessTime函數(shù)用于測試對結(jié)構(gòu)體數(shù)組的訪問時間 。通過運行這個程序,可以發(fā)現(xiàn)對齊后的結(jié)構(gòu)體數(shù)組訪問時間明顯比未對齊的要短,這直觀地展示了數(shù)據(jù)對齊對 CPU Cache 訪問效率和程序性能的提升作用 。在實際編程中,尤其是在對性能要求較高的場景下,合理地進行數(shù)據(jù)對齊是優(yōu)化程序性能的重要手段之一 。

5.2循環(huán)優(yōu)化

循環(huán)結(jié)構(gòu)在程序中非常常見,它對 CPU Cache 命中率有著顯著的影響 。當循環(huán)中的數(shù)據(jù)訪問模式不合理時,很容易導致 Cache 未命中次數(shù)增加,從而降低程序的執(zhí)行效率 。以下是一些優(yōu)化循環(huán)以提高 CPU Cache 命中率的方法和建議 。

減少循環(huán)嵌套深度:嵌套循環(huán)會增加數(shù)據(jù)訪問的復雜性,容易導致 Cache 命中率下降 。

例如,有一個雙重嵌套循環(huán):

for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        // 訪問數(shù)組a[i][j]
        data[i][j] = data[i][j] * 2;
    }
}

在這個例子中,如果N和M都比較大,那么在訪問data[i][j]時,由于數(shù)組元素在內(nèi)存中的存儲順序是按行優(yōu)先的,當內(nèi)層循環(huán)j不斷變化時,可能會頻繁地訪問不同的 Cache Line,導致 Cache 未命中次數(shù)增多 ??梢試L試將一些計算邏輯提取到外層循環(huán),減少內(nèi)層循環(huán)的復雜性,從而減少 Cache 未命中 。比如,如果內(nèi)層循環(huán)中有一些與j無關的計算,可以將其移到外層循環(huán):

// 假設存在一些與j無關的計算,這里簡化為一個常量計算
int constant = someComplexCalculation();
for (int i = 0; i < N; i++) {
    // 與j無關的計算結(jié)果在每次i循環(huán)中可以復用
    int temp = someCalculationBasedOnI(i, constant);
    for (int j = 0; j < M; j++) {
        // 利用與j無關的計算結(jié)果進行內(nèi)層循環(huán)操作
        data[i][j] = temp * data[i][j];
    }
}

合理安排循環(huán)順序:根據(jù)數(shù)據(jù)在內(nèi)存中的存儲方式和訪問的局部性原理,合理安排循環(huán)順序可以提高 Cache 命中率 。以二維數(shù)組為例,二維數(shù)組在內(nèi)存中是按行存儲的 。

如果按行優(yōu)先的順序訪問二維數(shù)組,更能利用空間局部性原理 。比如:

// 按行優(yōu)先訪問
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        sum += matrix[i][j];
    }
}

這種訪問方式下,當訪問matrix[i][j]時,由于空間局部性,matrix[i][j + 1]很可能也在同一個 Cache Line 中,從而提高了 Cache 命中率 。而如果按列優(yōu)先訪問:

// 按列優(yōu)先訪問
for (int j = 0; j < COLS; j++) {
    for (int i = 0; i < ROWS; i++) {
        sum += matrix[i][j];
    }
}

在這種情況下,每次訪問matrix[i][j]時,下一個訪問的matrix[i + 1][j]很可能在不同的 Cache Line 中,導致Cache未命中次數(shù)增加,降低了訪問效率 。所以,在大多數(shù)情況下,按行優(yōu)先訪問二維數(shù)組能更好地利用 CPU Cache,提高程序性能 。

在編寫程序時,充分考慮循環(huán)結(jié)構(gòu)對 CPU Cache 命中率的影響,并運用上述優(yōu)化方法,可以顯著提高程序的運行效率 。尤其是在處理大規(guī)模數(shù)據(jù)和對性能要求苛刻的應用場景中,循環(huán)優(yōu)化是提升程序性能的關鍵環(huán)節(jié)之一 。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2019-04-08 10:09:04

CPU緩存高性能

2019-12-10 08:10:35

LinuxCPU性能優(yōu)化

2019-07-26 06:30:37

CPU代碼操作系統(tǒng)

2023-02-02 08:04:15

Ceph數(shù)據(jù)CPU

2025-09-09 09:32:04

2020-09-23 09:21:56

CPUCache緩存

2012-10-09 09:43:50

WLAN優(yōu)化無線局域網(wǎng)WLAN

2021-02-02 13:45:31

Vue代碼前端

2025-02-25 12:00:00

Java線程開發(fā)

2012-07-23 10:22:15

Python性能優(yōu)化優(yōu)化技巧

2019-03-14 15:38:19

ReactJavascript前端

2009-05-08 09:01:03

微軟Windows 7操作系統(tǒng)

2011-10-19 09:41:15

ASP.NET性能優(yōu)化

2019-03-22 09:50:52

WebJavaScript前端

2020-06-11 13:03:04

性能優(yōu)化緩存

2009-04-16 17:24:54

性能優(yōu)化SQL Server 數(shù)據(jù)收集

2011-06-14 11:14:10

性能優(yōu)化代碼

2015-12-11 11:49:19

java

2015-12-11 11:39:15

.net代碼

2025-07-22 03:22:00

點贊
收藏

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