一、基本概念
大致關系: CPU Cache --> 前端總線 FSB (下圖中的Bus) --> Memory 內存
CPU 為了更快的執行代碼。於是當從內存中讀取數據時,並不是只讀自己想要的部分。而是讀取足夠的字節來填入高速緩存行。根據不同的 CPU ,高速緩存行大小不同。如 X86 是 32BYTES ,而 ALPHA 是 64BYTES 。並且始終在第 32 個字節或第 64 個字節處對齊。這樣,當 CPU 訪問相鄰的數據時,就不必每次都從內存中讀取,提高了速度。 因為訪問內存要比訪問高速緩存用的時間多得多。
1.1、總線概念
前端總線(FSB)就是負責將CPU連接到內存的一座橋,前端總線頻率則直接影響CPU與內存數據交換速度,如果FSB頻率越高,說明這座橋越寬,可以同時通過的車輛越多,這樣CPU處理的速度就更快。目前PC機上CPU前端總線頻率有533MHz、800MHz、1066MHz、1333MHz、1600MHz等幾種,前端總線頻率越高,CPU與內存之間的數據傳輸量越大。
前端總線——Front Side Bus(FSB),是將CPU連接到北橋芯片的總線。選購主板和CPU時,要注意兩者搭配問題,一般來說,前端總線是由CPU決定的,如果主板不支持CPU所需要的前端總線,系統就無法工作
1.2、 頻率與降頻概念
只支持1333內存頻率的cpu和主板配1600內存條就會降頻。核心數跟ddr2和ddr3沒關系,核心數是cpu本身的性質,cpu是四核的就是四核的,是雙核的就是雙核的。
如果只cpu支持1333,而主板支持1600,那也會降頻;cpu支持1600而主板只支持1333那不僅內存會降頻,而且發揮不出cpu全部性能。
另外如果是較新的主板cpu,已經采用新的qpi總線,而不是以前的fsb總線。
以前的fsb總線一般是總線為多少就支持多高的內存頻率。而qpi總線的cpu集成了內存控制器,5.0
gt/s的cpu可能只支持1333內存頻率,但是總線帶寬相當於1333內存的內存帶寬的兩倍,這時候,組成1333雙通道,內存速度就會翻倍,相當於2666的內存頻率。
1.3、cache line
Cache Line可以簡單的理解為CPU Cache中的最小緩存單位。目前主流的CPU Cache的Cache Line大小都是64Bytes。假設我們有一個512字節的一級緩存,那么按照64B的緩存單位大小來算,這個一級緩存所能存放的緩存個數就是512/64 = 8個。
1.4、cpu內存架構
1.5、CPU多級緩存架構-現代CPU多級緩存
高速緩存L1、L2、L3;
級別越小的緩存,越接近CPU, 意味着速度越快且容量越少。
L1是最接近CPU的,它容量最小,速度最快,每個核上都有一個L1 Cache(准確地說每個核上有兩個L1 Cache, 一個存數據 L1d Cache, 一個存指令 L1i Cache);
L2 Cache 更大一些,例如256K,速度要慢一些,一般情況下每個核上都有一個獨立的L2 Cache;二級緩存就是一級緩存的緩沖器:一級緩存制造成本很高因此它的容量有限,二級緩存的作用就是存儲那些CPU處理時需要用到、一級緩存又無法存儲的數據。
L3 Cache是三級緩存中最大的一級,例如12MB,同時也是最慢的一級,在同一個CPU插槽之間的核共享一個L3 Cache。三級緩存和內存可以看作是二級緩存的緩沖器,它們的容量遞增,但單位制造成本卻遞減。
當CPU運作時,它首先去L1尋找它所需要的數據,然后去L2,然后去L3。如果三級緩存都沒找到它需要的數據,則從內存里獲取數據。尋找的路徑越長,耗時越長。所以如果要非常頻繁的獲取某些數據,保證這些數據在L1緩存里。這樣速度將非常快。下表表示了CPU到各緩存和內存之間的大概速度:
從CPU到 大約需要的CPU周期 大約需要的時間(單位ns)
寄存器 1 cycle
L1 Cache ~3-4 cycles ~0.5-1 ns
L2 Cache ~10-20 cycles ~3-7 ns
L3 Cache ~40-45 cycles ~15 ns
跨槽傳輸 ~20 ns
內存 ~120-240 cycles ~60-120ns
另外需要注意的是,L3 Cache和L1,L2 Cache有着本質的區別。,L1和L2 Cache都是每個CPU core獨立擁有一個,而L3 Cache是幾個Cores共享的,可以認為是一個更小但是更快的內存。
可以通過cpu-z查看或者linux通過命令查看
有了上面對CPU的大概了解,我們來看看緩存行(Cache line)。緩存,是由緩存行組成的。一般一行緩存行有64字節(由上圖"64-byte line size"可知)。所以使用緩存時,並不是一個一個字節使用,而是一行緩存行、一行緩存行這樣使用;換句話說,CPU存取緩存都是按照一行,為最小單位操作的。
1.6、緩存一致性
試想下面這樣一個情況。
1、 CPU1 讀取了一個字節,以及它和它相鄰的字節被讀入 CPU1 的高速緩存。
2、 CPU2 做了上面同樣的工作。這樣 CPU1 , CPU2 的高速緩存擁有同樣的數據。
3、 CPU1 修改了那個字節,被修改后,那個字節被放回 CPU1 的高速緩存行。但是該信息並沒有被寫入 RAM 。
4、 CPU2 訪問該字節,但由於 CPU1 並未將數據寫入 RAM ,導致了數據不同步。
為了解決這個問題,芯片設計者制定了一個規則。當一個 CPU 修改高速緩存行中的字節時,計算機中的其它 CPU 會被通知,它們的高速緩存將視為無效。於是,在上面的情況下, CPU2 發現自己的高速緩存中數據已無效, CPU1 將立即把自己的數據寫回 RAM ,然后 CPU2 重新讀取該數據。 可以看出,高速緩存行在多處理器上會導致一些不利。
狀態 | 描述 | 監聽任務 |
---|---|---|
M 修改 (Modified) | 該Cache line有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。 | 緩存行必須時刻監聽所有試圖讀該緩存行相對就主存的操作,這種操作必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態之前被延遲執行。 |
E 獨享、互斥 (Exclusive) | 該Cache line有效,數據和內存中的數據一致,數據只存在於本Cache中。 | 緩存行也必須監聽其它緩存讀主存中該緩存行的操作,一旦有這種操作,該緩存行需要變成S(共享)狀態。 |
S 共享 (Shared) | 該Cache line有效,數據和內存中的數據一致,數據存在於很多核的Cache中。 | 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。 |
I 無效 (Invalid) | 該Cache line無效。 | 無 |
注意:
對於M和E狀態而言總是精確的,他們在和該緩存行的真正狀態是一致的,而S狀態可能是非一致的。如果一個緩存將處於S狀態的緩存行作廢了,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷為E狀態,這是因為其它緩存不會廣播他們作廢掉該緩存行的通知,同樣由於緩存並沒有保存該緩存行的copy的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該緩存行。
從上面的意義看來E狀態是一種投機性的優化:如果一個CPU想修改一個處於S狀態的緩存行,總線事務需要將所有該緩存行的copy變成invalid狀態,而修改E狀態的緩存不需要使用總線事務。

S:shared狀態:每個核心的緩存都一樣,且與內存一致:
M和I狀態:其中一個核心修改了值,那么其他的核心的緩存失效
四鍾狀態的更新路線圖
高效的狀態: E, S
低效的狀態: I, M
這四種狀態,保證CPU內部的緩存數據是一致的,但是,並不能保證是強一致性。
每個cache的控制器不僅知道自己的讀寫操作,而且也要監聽其它cache的讀寫操作。
理解該圖的前置說明:
1.觸發事件
觸發事件 | 描述 |
---|---|
本地讀取(Local read) | 本地cache讀取本地cache數據 |
本地寫入(Local write) | 本地cache寫入本地cache數據 |
遠端讀取(Remote read) | 其他cache讀取本地cache數據 |
遠端寫入(Remote write) | 其他cache寫入本地cache數據 |
2.cache分類:
前提:所有的cache共同緩存了主內存中的某一條數據。
本地cache:指當前cpu的cache。
觸發cache:觸發讀寫事件的cache。
其他cache:指既除了以上兩種之外的cache。
注意:本地的事件觸發 本地cache和觸發cache為相同。
上圖的切換解釋:
狀態 | 觸發本地讀取 | 觸發本地寫入 | 觸發遠端讀取 | 觸發遠端寫入 |
---|---|---|---|---|
M狀態(修改) | 本地cache:M 觸發cache:M 其他cache:I |
本地cache:M 觸發cache:M 其他cache:I |
本地cache:M→E→S 觸發cache:I→S 其他cache:I→S 同步主內存后修改為E獨享,同步觸發、其他cache后本地、觸發、其他cache修改為S共享 |
本地cache:M→E→S→I 觸發cache:I→S→E→M 其他cache:I→S→I 同步和讀取一樣,同步完成后觸發cache改為M,本地、其他cache改為I |
E狀態(獨享) | 本地cache:E 觸發cache:E 其他cache:I |
本地cache:E→M 觸發cache:E→M 其他cache:I 本地cache變更為M,其他cache狀態應當是I(無效) |
本地cache:E→S 觸發cache:I→S 其他cache:I→S 當其他cache要讀取該數據時,其他、觸發、本地cache都被設置為S(共享) |
本地cache:E→S→I 觸發cache:I→S→E→M 其他cache:I→S→I 當觸發cache修改本地cache獨享數據時時,將本地、觸發、其他cache修改為S共享.然后觸發cache修改為獨享,其他、本地cache修改為I(無效),觸發cache再修改為M |
S狀態(共享) | 本地cache:S 觸發cache:S 其他cache:S |
本地cache:S→E→M 觸發cache:S→E→M 其他cache:S→I 當本地cache修改時,將本地cache修改為E,其他cache修改為I,然后再將本地cache為M狀態 |
本地cache:S 觸發cache:S 其他cache:S |
本地cache:S→I 觸發cache:S→E→M 其他cache:S→I 當觸發cache要修改本地共享數據時,觸發cache修改為E(獨享),本地、其他cache修改為I(無效),觸發cache再次修改為M(修改) |
I狀態(無效) | 本地cache:I→S或者I→E 觸發cache:I→S或者I →E 其他cache:E、M、I→S、I 本地、觸發cache將從I無效修改為S共享或者E獨享,其他cache將從E、M、I 變為S或者I |
本地cache:I→S→E→M 觸發cache:I→S→E→M 其他cache:M、E、S→S→I |
既然是本cache是I,其他cache操作與它無關 | 既然是本cache是I,其他cache操作與它無關 |
下圖示意了,當一個cache line的調整的狀態的時候,另外一個cache line 需要調整的狀態。
M | E | S | I | |
---|---|---|---|---|
M | × | × | × | √ |
E | × | × | × | √ |
S | × | × | √ | √ |
I | √ | √ | √ | √ |
查看地址:https://www.cnblogs.com/jokerjason/p/9584402.html
3、內存分析
工具:Memory Analyzer Tool,MAT,或者JProfiler
分析代碼:

package com.lhx.cloud; /** * @author lihongxu * @since 2019/4/5 上午10:39 */ public class L1CacheMiss { private static final int RUNS = 10; private static final int DIMENSION_1 = 1024 * 1024; private static final int DIMENSION_2 = 6; private static long[][] longs; public static void main(String[] args) throws Exception { Thread.sleep(10000); longs = new long[DIMENSION_1][]; for (int i = 0; i < DIMENSION_1; i++) { longs[i] = new long[DIMENSION_2]; for (int j = 0; j < DIMENSION_2; j++) { longs[i][j] = 1L; } } System.out.println("starting...."); long sum = 0L; for (int r = 0; r < RUNS; r++) { final long start = System.nanoTime(); //// slow // for (int j = 0; j < DIMENSION_2; j++) { // for (int i = 0; i < DIMENSION_1; i++) { // sum += longs[i][j]; // } // } //fast for (int i = 0; i < DIMENSION_1; i++) { for (int j = 0; j < DIMENSION_2; j++) {//每次取出6個相加 sum += longs[i][j]; } } System.out.println((System.nanoTime() - start)/1000000.0); } System.out.println("sum:"+sum); while (true){ Thread.sleep(1000); } } }
64位系統,Java數組對象頭固定占16字節(壓縮后16,壓縮前24,默認壓縮),而long類型占8個字節。所以16+8*6=64字節,剛好等於一條緩存行的長度:
每次開始內循環時,從內存抓取的數據塊實際上覆蓋了longs[i][0]到longs[i][5]的全部數據(剛好64字節)。因此,內循環時所有的數據都在L1緩存可以命中,遍歷將非常快。
那么將會造成大量的緩存失效。因為每次從內存抓取的都是同行不同列的數據塊(如longs[i][0]到longs[i][5]的全部數據),但循環下一個的目標,卻是同列不同行(如longs[0][0]下一個是longs[1][0],造成了longs[0][1]-longs[0][5]無法重復利用)。運行時間的差距如下圖,單位是微秒(us):
最后,我們都希望需要的數據都在L1緩存里,但事實上經常事與願違,所以緩存失效 (Cache Miss)是常有的事,也是我們需要避免的事。
一般來說,緩存失效有三種情況:
1. 第一次訪問數據, 在cache中根本不存在這條數據, 所以cache miss, 可以通過prefetch解決。
2. cache沖突, 需要通過補齊來解決(偽共享的產生)。
3. cache滿, 一般情況下我們需要減少操作的數據大小, 盡量按數據的物理順序訪問數據。