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

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

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

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

1. 介紹

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

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

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

高速緩存中的數據寫回主存並不是當即執行,寫回主存的時機:sass

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%左右,L二、L3的機制也相似。這樣一來,CPU須要在主存中讀取的數據大概爲5%-10%,其他命中所有能夠在L一、L二、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語言)

int testfun(int x)
{
   if(__builtin_expect(x, 0)) {
    ^^^--- We instruct the compiler, "else" block is more probable
    x = 5;
    x = x * x;
   } else {
    x = 6;
   }
   return x;
}
在這個例子中,x爲0的可能性更大,編譯後觀察彙編指令,結果以下:
Disassembly of section .text:
00000000 <testfun>:
   0:   55             push   %ebp
   1:   89 e5            mov    %esp,%ebp
   3:   8b 45 08          mov    0x8(%ebp),%eax
   6:   85 c0            test   %eax,%eax
   8:   75 07            jne    11 <testfun+0x11>
   a:   b8 06 00 00 00       mov    $0x6,%eax
   f:   c9             leave
  10:   c3             ret
  11:   b8 19 00 00 00       mov    $0x19,%eax
  16:   eb f7            jmp    f <testfun+0xf>

能夠看到,編譯器使用的是 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高速緩存與極性代碼設計

 

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

相關文章
相關標籤/搜索