CPU高速緩存與極性代碼設計


摘要:CPU內置少量的高速緩存的重要性不言而喻,在體積、成本、效率等因素下產生了當今用到的計算機的存儲結構。
  1. 介紹
  2. cpu緩存的結構
  3. 緩存的存取與一致
  4. 代碼設計的考量
  5. 最后

CPU頻率太快,其處理速度遠快於存儲介質的讀寫。因此,導致CPU資源的浪費,需要有效解決IO速度和CPU運算速度之間的不匹配問題。芯片級高速緩存可大大減少之間的處理延遲。CPU制造工藝的進步使得在比以前更小的空間中安裝數十億個晶體管,如此可為緩存留出更多空間,使其盡可能地靠近核心。

CPU內置少量的高速緩存的重要性不言而喻,在體積、成本、效率等因素下產生了當今用到的計算機的存儲結構。

1. 介紹

計算機的內存具有基於速度的層次結構,CPU高速緩存位於該層次結構的頂部,是介於CPU內核和物理內存(動態內存DRAM)之間的若干塊靜態內存,是最快的。它也是最靠近中央處理的地方,是CPU本身的一部分,一般直接跟CPU芯片集成。

CPU計算:程序被設計為一組指令,最終由CPU運行。

裝載程序和數據,先從最近的一級緩存讀取,如有就直接返回,逐層讀取,直至從內存及其它外部存儲中加載,並將加載的數據依次放入緩存。

高速緩存中的數據寫回主存並非立即執行,寫回主存的時機:

1.緩存滿了,采用先進先出或最久未使用的順序寫回;

2.#Lock信號,緩存一致性協議,明確要求數據計算完成后要立馬同步回主存。

2. CPU緩存的結構

現代的CPU緩存結構被分為多處理、多核、多級的層次。

2.1 多級緩存結構

分為三個主要級別,即L1,L2和L3。離CPU越近的緩存,讀取效率越高,存儲容量越小,造價越高。

L1高速緩存是系統中存在的最快的內存。就優先級而言,L1緩存具有CPU在完成特定任務時最可能需要的數據,大小通常可達256KB,一些功能強大的CPU占用近1MB。某些服務器芯片組(如Intel高端Xeon CPU)具有1-2MB。L1緩存通常又分為指令緩存和數據緩存。指令緩存處理有關CPU必須執行的操作的信息,數據緩存則保留要在其上執行操作的數據。如此,減少了爭用Cache所造成的沖突,提高了處理器效能。

L2級緩存比L1慢,但大小更大,通常在256KB到8MB之間,功能強大的CPU往往會超過此大小。L2高速緩存保存下一步可能由CPU訪問的數據。大多數CPU中,L1和L2高速緩存位於CPU內核本身,每個內核都有自己的高速緩存。

L3級高速緩存是最大的高速存儲單元,也是最慢的。大小從4MB到50MB以上。現代CPU在CPU裸片上具有用於L3高速緩存的專用空間,且占用了很大一部分空間。

2.2 多處理器緩存結構

計算機早已進入多核時代,軟件也運行在多核環境。一個處理器對應一個物理插槽、包含多個核(一個核包含寄存器、L1 Cache、L2 Cache),多核間共享L3 Cache,多處理器間通過QPI總線相連。

L1 和 L2 緩存為CPU單個核心私有的緩存,L3 緩存是同插槽的所有核心都共享的緩存。

L1緩存被分成獨立的32K數據緩存和32K指令緩存,L2緩存被設計為L1與共享的L3緩存之間的緩沖。大小為256K,主要作為L1和L3之間的高效內存訪問隊列,同時包含數據和指令。L3緩存包括了在同一個槽上的所有L1和L2緩存中的數據。這種設計消耗了空間,但可攔截對L1和L2緩存的請求,減輕了各個核心私有的L1和L2緩存的負擔。

2.3 Cache Line

Cache存儲數據以固定大小為單位,稱為Cache Line/Block。給定容量和Cache Line size,則就固定了存儲的條目個數。對於X86,Cache Line大小與DDR一次訪存能得到的數據大小一致,即64B。舊的ARM架構的Cache Line是32B,因此經常是一次填兩個Cache Line。CPU從Cache獲取數據的最小單位為字節,Cache從Memory獲取的最小單位是Cache Line,Memory從磁盤獲取數據通常最小是4K。

Cache分成多個組,每組分成多個Cache Line行,大致如下圖:

Linux系統下使用以下命令查看Cache信息,lscpu命令也可。

3. 緩存的存取與一致

下面表格描述了不同存儲介質的存取信息,供參考。

存取速度:寄存器 > cache(L1~L3) > RAM > Flash > 硬盤 > 網絡存儲

以2.2Ghz頻率的CPU為例,每個時鍾周期大概是0.5納秒。

3.1 讀取存儲器數據

按CPU層級緩存結構,取數據的順序是先緩存再主存。當然,如數據來自寄存器,只需直接讀取返回即可。

  • 1) 如CPU要讀的數據在L1 cache,鎖住cache行,讀取后解鎖、返回
  • 2) 如CPU要讀的數據在L2 cache,數據在L2里加鎖,將數據復制到L1,再執行讀L1
  • 3) 如CPU要讀的數據在L3 cache,也一樣,只不過先由L3復制到L2,再從L2復制到L1,最后從L1到CPU
  • 4) 如CPU需讀取內存,則首先通知內存控制器占用總線帶寬,后內存加鎖、發起讀請求、等待回應,回應數據保存至L3,L2,L1,再從L1到CPU后解除總線鎖定。

3.2 緩存命中與延遲

由於數據的局部性原理,CPU往往需要在短時間內重復多次讀取數據,內存的運行頻率遠跟不上CPU的處理速度,緩存的重要性被凸顯。CPU可避開內存在緩存里讀取到想要的數據,稱之為命中。L1的運行速度很快,但容量很小,在L1里命中的概率大概在80%左右,L2、L3的機制也類似。這樣一來,CPU需要在主存中讀取的數據大概為5%-10%,其余命中全部可以在L1、L2、L3中獲取,大大減少了系統的響應時間。

高速緩存旨在加快主內存和CPU之間的數據傳輸。從內存訪問數據所需的時間稱為延遲,L1具有最低延遲且最接近核心,而L3具有最高的延遲。緩存未命中時,由於CPU需從主存儲器中獲取數據,導致延遲會更多。

3.3 緩存替換策略

Cache里的數據是Memory中常用數據的一個拷貝,存滿后再存入一個新的條目時,就需要把一個舊的條目從緩存中拿掉,這個過程稱為evict。緩存管理單元通過一定的算法決定哪些數據需要從Cache里移出去,稱為替換策略。最簡單的策略為LRU,在CPU設計的過程中,通常會對替換策略進行改進,每一款芯片幾乎都使用了不同的替換策略。

3.4 MESI緩存一致性

在多CPU的系統中,每個CPU都有自己的本地Cache。因此,同一個地址的數據,有可能在多個CPU的本地 Cache 里存在多份拷貝。為了保證程序執行的正確性,就必須保證同一個變量,每個CPU看到的值都是一樣的。也就是說,必須要保證每個CPU的本地Cache中能夠如實反映內存中的真實數據。

假設一個變量在CPU0和CPU1的本地Cache中都有一份拷貝,當CPU0修改了這個變量時,就必須以某種方式通知CPU1,以便CPU1能夠及時更新自己本地Cache中的拷貝,這樣才能在兩個CPU之間保持數據的同步,CPU之間的這種同步有較大開銷。

為保證緩存一致,現代CPU實現了非常復雜的多核、多級緩存一致性協議MESI, MESI具體的操作上會針對單個緩存行進行加鎖。

MESI:Modified Exclusive Shared or Invalid

1) 協議中的狀態

CPU中每個緩存行使用4種狀態進行標記(使用額外的兩位bit表示)

M: Modified

該緩存行只被緩存在該CPU的緩存中,且被修改過(dirty),即與主存中的數據不一致,該緩存行中的內容需在未來的某個時間點(允許其它CPU讀取主存中相應內存之前)寫回主存。當被寫回主存之后,該緩存行的狀態變成獨享(exclusive)狀態

E: Exclusive

該緩存行只被緩存在該CPU的緩存中,未被修改,與主存中數據一致。在任何時刻當有其它CPU讀取該內存時變成shared狀態。同樣,當修改該緩存行中內容時,該狀態可以變成Modified狀態.

S: Shared

意味該緩存行可能被多個CPU緩存,各個緩存中的數據與主存數據一致,當有一個CPU修改該緩存行中,其它CPU中該緩存行可以被作廢(Invalid).

I: Invalid,緩存無效(可能其它CPU修改了該緩存行)

2) 狀態切換關系

由下圖可看出cache是如何保證它的數據一致性的。

譬如,當前核心要讀取的數據塊在其核心的cache狀態為Invalid,在其他核心上存在且狀態為Modified的情況。可以從當前核心和其它核心兩個角度觀察,其它核心角度:當前狀態為Modified,其它核心想讀這個數據塊(圖中Modified到Shared的綠色虛線):先把改變后的數據寫入到內存中(先於其它核心的讀),並更新該cache狀態為Share.當前核心角度:當前狀態為Invalid,想讀這個數據塊(圖中Invalid到Shared的綠色實線):這種情況下會從內存中重新加載,並更新該cache狀態Share

以下表格從這兩個角度列舉了所有情況,供參考:

3) 緩存的操作描述

一個典型系統中會有幾個緩存(每個核心都有)共享主存總線,每個相應的CPU會發出讀寫請求,而緩存的目的是為了減少CPU讀寫共享主存的次數。

  • 一個緩存除在Invalid狀態外都可以滿足CPU的讀請求,一個Invalid的緩存行必須從主存中讀取(變成S或 E狀態)來滿足該CPU的讀請求。
  • 一個寫請求只有在該緩存行是M或E狀態時才能被執行,如果緩存行處於S狀態,必須先將其它緩存中該緩存行變成Invalid(不允許不同CPU同時修改同一緩存行,即使修改該緩存行中不同位置的數據也不可),該操作常以廣播方式來完成。
  • 緩存可以隨時將一個非M狀態的緩存行作廢,或變成Invalid,而一個M狀態的緩存行必須先被寫回主存。一個處於M狀態的緩存行必須時刻監聽所有試圖讀該緩存行相對主存的操作,操作必須在緩存將該緩存行寫回主存並將狀態變成S狀態之前被延遲執行。
  • 一個處於S狀態的緩存行需監聽其它緩存使該緩存行無效或獨享該緩存行的請求,並將該緩存行變成無效。
  • 一個處於E狀態的緩存行也必須監聽其它讀主存中該緩存行的操作,一旦有這種操作,該緩存行需變成S狀態。
  • 對於M和E狀態而言總是精確的,和該緩存行的真正狀態是一致的。而S狀態可能是非一致的,如果一個緩存將處於S狀態的緩存行作廢了,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷為E狀態,是因為其它緩存不會廣播作廢掉該緩存行的通知,同樣,由於緩存並沒有保存該緩存行的copy的數量,因此也沒有辦法確定自己是否已經獨享了該緩存行。

從上面的意義來看,E狀態是一種投機性的優化:如果一個CPU想修改一個處於S狀態的緩存行,總線事務需要將所有該緩存行的copy變成Invalid狀態,而修改E狀態的緩存不需要使用總線事務。

4. 代碼設計的考量

理解計算機存儲器層次結構對應用程序的性能影響。如果需要的程序在CPU寄存器中,指令執行時1個周期內就能訪問到;如果在CPU Cache中,需1~30個周期;如果在主存中,需要50~200個周期;在磁盤上,大概需要萬級周期。另外,Cache Line的存取也是代碼設計者需要關注的部分, 以規避偽共享的執行場景。因此,充分利用緩存的結構和機制可有效提高程序的執行性能。

4.1 局部性特性

一旦CPU要從內存或磁盤中訪問數據就會產生一個很大的時延,程序性能顯著降低,為此我們不得不提高Cache命中率,也就是充分發揮局部性原理。一般來說,具有良好局部性的程序會比局部性較差的程序運行得更快,程序性能更好。

局部性機制確保在訪問存儲設備時,存取數據或指令都趨於聚集在一片連續的區域。一個設計優良的計算機程序通常具有很好的局部性,時間局部性和空間局部性。

1) 時間局部性

如果一個數據/信息項被訪問過一次,那么很有可能它會在很短的時間內再次被訪問。比如循環、遞歸、方法的反復調用等。

2) 空間局部性

一個Cache Line有64字節塊,可以充分利用一次加載64字節的空間,把程序后續會訪問的數據,一次性全部加載進來,從而提高Cache Line命中率(而非重新去尋址讀取)。如果一個數據被訪問,那么很有可能位於這個數據附近的其它數據也會很快被訪問到。比如順序執行的代碼、連續創建的多個對象、數組等。數組就是一種把局部性原理利用到極致的數據結構。

3) 代碼示例

示例1,(C語言)

//程序 array1.c  多維數組交換行列訪問順序
char array[10240][10240];
 
int main(int argc, char *argv[]){
int i = 0;
int j = 0;
for(i=0; i < 10240 ; i++) {
for(j=0; j < 10240 ; j++) {
  array[i][j] = ‘A’; //按行進行訪問
}
}
return 0;
}
//程序array2.c

紅色字體的代碼調整為: array[j][i] = ‘A’; //按列進行訪問

編譯、運行結果如下:

從測試結果看,第一個程序運行耗時0.265秒,第二個1.998秒,是第一個程序的7.5倍。

案例參考:https://www.cnblogs.com/wanghuaijun/p/12904159.html

結果分析

數組元素存儲在地址連續的內存中,多維數組在內存中是按行進行存儲。第一個程序按行訪問某個元素時,該元素附近的一個Cache Line大小的元素都會被加載到Cache中,這樣一來,在訪問緊挨着的下一個元素時,就可直接訪問Cache中的數據,不需再從內存中加載。也就是說,對數組按行進行訪問時,具有更好的空間局部性,Cache命中率更高。

第二個程序按列訪問某個元素,雖然該元素附近的一個Cache Line大小的元素也會被加載進Cache中,但接下來要訪問的數據卻不是緊挨着的那個元素,因此很有可能會再次產生Cache miss,而不得不從內存中加載數據。而且,雖然Cache中會盡量保存最近訪問過的數據,但Cache大小有限,當Cache被占滿時,就不得不把一些數據給替換掉。這也是空間局部性差的程序更容易產生Cache miss的重要原因之一。

示例2,(Java)

以下代碼中長度為16的row和column數組,在Cache Line 64字節數據塊上內存地址是連續的,能被一次加載到Cache Line中,在訪問數組時命中率高,性能發揮到極致。

public int run(int[] row, int[] column) {
    int sum = 0;
    for(int i = 0; i < 16; i++ ) {
        sum += row[i] * column[i];
    }
    return sum;
}

變量i體現了時間局部性,作為計數器被頻繁操作,一直存放在寄存器中,每次從寄存器訪問,而不是從緩存或主存訪問。

4.2 緩存行的鎖競爭

在多處理器下,為保證各個處理器的緩存一致,會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存里把數據讀到處理器緩存里。

當多個線程對同一個緩存行訪問時,如其中一個線程鎖住緩存行,然后操作,這時其它線程則無法操作該緩存行。這種情況下,我們在進行程序代碼設計時是要盡量避免的。

4.3 偽共享的規避

連續緊湊的內存分配帶來高性能,但並不代表它一直都行之有效,偽共享就是無聲的性能殺手。所謂偽共享(False Sharing),是由於運行在不同CPU上的不同線程,同時修改處在同一個Cache Line上的數據引起。緩存行上的寫競爭是運行在SMP系統中並行線程實現可伸縮性最重要的限制因素,一般來說,從代碼中很難看清是否會出現偽共享。

在每個CPU來看,各自修改的是不同的變量,但由於這些變量在內存中彼此緊挨着,因此它們處於同一個Cache Line上。當一個CPU修改這個Cache Line之后,為了保證數據的一致性,必然導致另一個CPU的本地Cache的無效,因而觸發Cache miss,然后從內存中重新加載變量被修改后的值。多個線程頻繁的修改處於同一個Cache Line的數據,會導致大量的Cache miss,因而造成程序性能的大幅下降。

下圖說明了兩個不同Core的線程更新同一緩存行的不同信息項:

上圖說明了偽共享的問題。在Core1上運行的線程准備更新變量X,同時Core2上的線程准備更新變量Y。然而,這兩個變量在同一個緩存行中。每個線程都要去競爭緩存行的所有權來更新變量。如果Core1獲得了所有權,緩存子系統將會使Core2中對應的緩存行失效。當Core2獲得了所有權然后執行更新操作,Core1就要使自己對應的緩存行失效。來來回回的經過L3緩存,大大影響了性能。如果互相競爭的Core位於不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴重。

1) 規避處理方式

  • 增大數組元素的間隔使得不同線程存取的元素位於不同cache line,空間換時間
  • 在每個線程中創建全局數組各個元素的本地拷貝,然后結束后再寫回全局數組

2) 代碼示例說明

示例3,(JAVA)

從代碼設計角度,要考慮清楚類結構中哪些變量是不變,哪些是經常變化,哪些變化是完全相互獨立,哪些屬性一起變化。假如業務場景中,下面的對象滿足幾個特點

public class Data{
    long modifyTime;
    boolean flag;
    long createTime;
    char key;
    int value;
}
  • 當value變量改變時,modifyTime肯定會改變
  • createTime變量和key變量在創建后就不會再變化
  • flag也經常會變化,不過與modifyTime和value變量毫無關聯

當上面的對象需要由多個線程同時訪問時,從Cache角度,當我們沒有加任何措施時,Data對象所有的變量極有可能被加載在L1緩存的一行Cache Line中。在高並發訪問下,會出現這種問題:

如上圖所示,每次value變更時,根據MESI協議,對象其他CPU上相關的Cache Line全部被設置為失效。其他的處理器想要訪問未變化的數據(key和createTime)時,必須從內存中重新拉取數據,增大了數據訪問的開銷。

有效的Padding方式

正確方式是將該對象屬性分組,將一起變化的放在一組,與其他無關的放一組,將不變的放到一組。這樣當每次對象變化時,不會帶動所有的屬性重新加載緩存,提升了讀取效率。在JDK1.8前,一般在屬性間增加長整型變量來分隔每一組屬性。被操作的每一組屬性占的字節數加上前后填充屬性所占的字節數,不小於一個cache line的字節數就可達到要求。

public class DataPadding{
    long a1,a2,a3,a4,a5,a6,a7,a8;//防止與前一個對象產生偽共享
    int value;
    long modifyTime;
    long b1,b2,b3,b4,b5,b6,b7,b8;//防止不相關變量偽共享;
    boolean flag;
    long c1,c2,c3,c4,c5,c6,c7,c8;//
    long createTime;
    char key;
    long d1,d2,d3,d4,d5,d6,d7,d8;//防止與下一個對象產生偽共享
}

采取上述措施后的圖示:

在Java中

Java8實現字節填充避免偽共享, JVM參數 -XX:-RestrictContended

@Contended 位於sun.misc用於注解java 屬性字段,自動填充字節,防止偽共享。

示例4,(C語言)

//程序 thread1.c   多線程訪問數據結構的不同字段
#include <stdio.h>
#include <pthread.h>
 
struct {
   int a;
   // char padding[64]; // thread2.c代碼
   int b;
}data;
 
void *thread_1(void) {
    int i = 0;
for(i=0; i < 1000000000; i++){
    data.a = 0;
}
}
void *thread_2(void) {
    int i = 0;
for(i=0; i < 1000000000; i++){
    data.b = 0;
}
}
 
//程序 thread2.c
Thread1.c 中的紅色字體行,打開注釋即為 thread2.c
main()函數很簡單,創建兩個線程並運行,參考代碼如下:
pthread_t id1;
int ret = pthread_create(&id1,NULL, (void*)thread_1, NULL);

編譯、運行結果如下:

從測試結果看,第一個程序消耗的時間是第二個程序的3倍

案例參考:https://www.cnblogs.com/wanghuaijun/p/12904159.html

結果分析

此示例涉及到Cache Line的偽共享問題。兩個程序唯一的區別是,第二個程序中字段a和b中間有一個大小為64個字節的字符數組。第一個程序中,字段a和字段b處於同一個Cache Line上,當兩個線程同時修改這兩個字段時,會觸發Cache Line偽共享問題,造成大量的Cache miss,進而導致程序性能下降。

第二個程序中,字段a和b中間加了一個64字節的數組,這樣就保證了這兩個字段處在不同的Cache Line上。如此一來,兩個線程即便同時修改這兩個字段,兩個cache line也互不影響,cache命中率很高,程序性能會大幅提升。

示例5,(C語言)

在設計數據結構的時候,盡量將只讀數據與讀寫數據分開,並具盡量將同一時間訪問的數據組合在一起。這樣CPU能一次將需要的數據讀入。譬如,下面的數據結構就很不好。

struct __a
{
   int id; // 不易變
   int factor;// 易變
   char name[64];// 不易變
   int value;// 易變
};
在 X86 下,可以試着修改和調整它
#define CACHE_LINE_SIZE 64  //緩存行長度
struct __a
{
   int id; // 不易變
   char name[64];// 不易變
  char __align[CACHE_LINE_SIZE – sizeof(int)+sizeof(name) * sizeof(name[0]) % CACHE_LINE_SIZE]
   int factor;// 易變
   int value;// 易變
   char __align2[CACHE_LINE_SIZE –2* sizeof(int)%CACHE_LINE_SIZE ]
};

CACHE_LINE_SIZE–sizeof(int)+sizeof(name)*sizeof(name[0])%CACHE_LINE_SIZE看起來不和諧,CACHE_LINE_SIZE表示高速緩存行(64B大小)。__align用於顯式對齊,這種方式使得結構體字節對齊的大小為緩存行的大小。

4.4 緩存與內存對齊

1)字節對齊

__attribute__ ((packed))告訴編譯器取消結構在編譯過程中的優化對齊,按照實際占用字節數進行對齊,是GCC特有的語法;

__attribute__((aligned(n)))表示所定義的變量為n字節對齊;

struct B{ char b;int a;short c;}; (默認4字節對齊)

這時候同樣是總共7個字節的變量,但是sizeof(struct B)的值卻是12。

字節對齊的細節和編譯器實現相關,但一般而言,滿足三個准則:

  1. (結構體)變量的首地址能夠被其(最寬)基本類型成員的大小所整除;
  2. 結構體每個成員相對於首地址的偏移量都是成員大小的數倍,如有需要,編譯器會在成員之間加上填充字節(internal adding)
  3. 結構體的總大小為結構體最寬基本類型成員大小的數倍,如有需要,編譯器會在最末一個成員之后加上填充字節(trailing padding)

2)緩存行對齊

數據跨越兩個cache line,意味着兩次load或兩次store。如果數據結構是cache line對齊的,就有可能減少一次讀寫。數據結構的首地址cache line對齊,意味着可能有內存浪費(特別是數組這樣連續分配的數據結構),所以需要在空間和時間兩方面權衡。比如現在一般的malloc()函數,返回的內存地址會已經是8字節對齊的,這就是為了能夠讓大部分程序有更好的性能。

在C語言中,為了避免偽共享,編譯器會自動將結構體,字節補全和對齊,對齊的大小最好是緩存行的長度。總的來說,結構體實例會和它的最寬成員一樣對齊。編譯器這樣做是因為這是保證所有成員自對齊以獲得快速存取的最容易方法。

__attribute__((aligned(cache_line)))對齊實現
struct syn_str { ints_variable; };__attribute__((aligned(cache_line)));

示例6,(C語言)

struct syn_str { int s_variable; };
void *p = malloc ( sizeof (struct syn_str) + cache_line );
syn_str *align_p=(syn_str*)((((int)p)+(cache_line-1))&~(cache_line-1);

4.5 CPU分支預測

代碼在內存里面是順序排列的,可以順序訪問,有效提高緩存命中。對於分支程序來說,如果分支語句之后的代碼有更大的執行幾率,就可以減少跳轉,一般CPU都有指令預取功能,這樣可以提高指令預取命中的幾率。分支預測用的就是likely/unlikely這樣的宏,一般需要編譯器的支持,屬靜態的分支預測。現在也有很多CPU支持在內部保存執行過的分支指令的結果(分支指令cache),所以靜態的分支預測就沒有太多的意義。

示例7,(C語言)

struct syn_str { int s_variable; };
void *p = malloc ( sizeof (struct syn_str) + cache_line );
syn_str *align_p=(syn_str*)((((int)p)+(cache_line-1))&~(cache_line-1);

可以看到,編譯器使用的是 jne指令,且else block中的代碼緊跟在后面

8:   75 07              jne    11 <testfun+0x11>
a:   b8 06 00 00 00          mov    $0x6,%eax

4.6 命中率的監控

程序設計要追求更好的利用CPU緩存,來減少從內存讀取數據的低效。在程序運行時,通常需要關注緩存命中率這個指標。

監控方法(Linux):查詢CPU緩存無命中次數及緩存請求次數,計算緩存命中率

perf stat -e cache-references -e cache-misses 

4.7 小 結

程序從內存獲取數據時,每次不僅取回所需數據,還會根據緩存行的大小加載一段連續的內存數據到緩存中,程序設計中的優化范式參考如下。

  • 在集合遍歷的場景,可使用有序數組,數組在內存中是一段連續的空間;
  • 字段盡量定義為占用字節小的類型,如int可滿足時,不使用long。這樣單次可加載更多的數據到緩存中;
  • 對象或結構體大小盡可能設置為CPU核心緩存行的整數倍。基於64B大小的緩存行,如讀取的數據是50B,最壞情況下需要兩次從內存加載;當為70B時,最壞情況需要三次內存讀取,才能加載到緩存中;
  • 對同一對象/結構體的多個屬性,可能存在於同一緩存行中,導致偽共享問題,需為屬性的不變與常變,變化的獨立與關聯而隔離設計,及緩存行對齊,解決多線程高並發環境下緩存失效、彼此牽制問題;
  • CPU有分支預測能力,在使用ifelse case when等循環判斷的場景時,可以順序訪問,有效提高緩存的命中

. . . . . .

除了本章節中介紹的案例之外,在系統中CPU Cache對程序性能的影響隨處可見。

5. 最后

CPU高速緩存可以說是CPU與內存之間的臨時數據交換器,用以有效解決CPU運行處理速度與內存讀寫速度不匹配的矛盾。緩存設計也一直在發展,尤其是隨着內存變得更便宜、更快和更密集。減少內存的延遲必定可以減少現代計算機的瓶頸。

CPU的高速緩存設計,多處理器、多核的多級緩存,帶來高效處理的同時也引入了緩存數據的存取與一致性問題,現代CPU實現了非常復雜的多核、多級緩存一致性協議MESI,以保證多個CPU高速緩存之間共享數據的一致。

CPU高速緩存往往需要重復處理相同的數據、執行相同的指令,如果這部分數據、指令都能在高速緩存中找到,即可減少整機的響應時間。程序的設計需要確保充分利用高速緩存的結構與機制,需要利用處理的局部特性,規避鎖競爭、偽共享、緩存的失效、彼此的牽制,權衡空間與時間,獲取程序運行時的極致與高性能。

. . . . . .

參考博文

【系統性能專題一】CPU消耗及問題定位那點事

【系統性能專題二】性能測試及指標分析這點事

【系統性能專題三】CPU高速緩存與極性代碼設計

 

點擊關注,第一時間了解華為雲新鮮技術~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM