1. 程式人生 > >利用cpu快取實現高效能程式

利用cpu快取實現高效能程式

我們選購電腦時,CPU處理器的配置會有快取大小,它是CPU效能的重要指標。

intel官網cpu列表中的快取-1

為什麼呢?因為CPU計算速度與訪問主存速度非常不匹配!

先來看計算速度。單顆CPU計算速度目前在2GHz-4GHz之間,以2.5GHz計即每秒鐘計算25億次,每個時鐘週期耗時1/2.5GHz==0.4納秒。當前所有的計算機都遵循馮諾依曼結構,所以執行任何指令(例如加法操作)的流程必然遵循下圖:

cpu指令的執行過程

所以,做一次加法的指令是由多個時鐘週期組成的(如取指令和數字、放入暫存器、執行ALU、將結果寫回主存),做ALU執行指令僅需要1個時鐘週期,而取指令或者取資料、回寫結果資料就需要與主存打交道了。CPU訪問記憶體(主存)的速度非常慢,訪問一次常常需要上百納秒以上,這與計算指令有千倍的差距!怎樣解決訪問主存慢導致的CPU計算能力的浪費

呢?加入CPU快取!

CPU上增加快取後,由於CPU快取離CPU核心更近,所以訪問速度比主存快得多!如果我們訪問記憶體時,先把資料讀取到CPU快取再計算,而下次讀取到該資料時直接使用快取(若未被淘汰掉),這在時間和空間上都會降低CPU計算能力的浪費!在時間上,有些資料訪問頻率高(熱點),多次訪問之間都未被淘汰出快取;在空間上,快取可以同時載入相鄰的資料、程式碼,這樣函式、迴圈的執行都在使用快取中的資料。

CPU快取是分為多級的,原因是熱點資料太大了!最快的快取一定離CPU核心最近,因為體積小所以容量也最小,不能滿足以MB計算的熱點資料。最終發展出了三級快取,分別稱為L1、L2、L3級快取。這三級快取的訪問速度各不相同,但都遠大於訪問主存的速度(訪問時間更小),如下圖所示:

cpu各級快取的訪問速度

可見,L1和L2的快取訪問速度非常快,只有不到3ns,L3稍慢一些,但都遠小於訪問主存的速度。當然,CPU快取的大小也遠小於主存的大小,如本文最開始的那張圖,現在的CPU快取往往只有幾十MB。如果大家點選具體的CPU細看快取,可以看到intel只標明瞭smart cache,如下圖所示(intel e5-2620 v4):

intel-smart-cache-e5-2620v4

這個smart cache其實就是L3快取,現在的CPU都是多核心的,而smart cache就是智慧的被多CPU核心共用的意思。那麼L1、L2快取大小為什麼不標出來呢?其實沒有必要,因為通常L1就是32KB,而L2是256KB,在linux上我們可以直接看到:

model name	: Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz
[[email protected] ~]# cat /sys/devices/system/cpu/cpu1/cache/index0/size 
32K
[[email protected] ~]# cat /sys/devices/system/cpu/cpu1/cache/index1/size 
32K
[[email protected] ~]# cat /sys/devices/system/cpu/cpu1/cache/index2/size 
256K
[[email protected] ~]# cat /sys/devices/system/cpu/cpu1/cache/index3/size 
20480K

這裡,index0和index1分別代表L1快取中的指令快取和資料快取,index2是L2快取,index3就是L3快取。也可能一個快取由多個CPU共享,仍然以E5-2620 v4這個8核16執行緒的CPU為例:

[[email protected] ~]# cat /sys/devices/system/cpu/cpu0/cache/index1/shared_cpu_list 
0,16
[[email protected] ~]# cat /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_list 
0,16
[[email protected] ~]# cat /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list 
0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30

筆者的伺服器有兩顆e5,所以表現為32顆邏輯CPU。由於intel的超執行緒技術,所以兩顆邏輯CPU對應一顆物理CPU。簡單插一下何謂超執行緒技術:由於訪問主存的速度太慢,所以intel想了一個主意,就是當CPU在等待從主存中調入資料或者指令時,同時做另一個任務,這樣一顆CPU就表現為兩顆邏輯CPU,如下圖所示:

intel超執行緒技術-1

shared_cpu_list可見,20MB的L3快取被16顆邏輯CPU(8顆物理CPU)共享,而L2和L1都是由一顆物理CPU獨佔的。

CPU快取與主存交換資料每次大小是固定的,我們稱其為cpu cache line,在64位系統下通常是64位元組,在linux下可以這麼獲取該值:

[[email protected] ~]# cat /sys/devices/system/cpu/cpu1/cache/index3/coherency_line_size 
64

在C語言程式裡,可以通過sysconf (_SC_LEVEL1_DCACHE_LINESIZE)獲取,例如在nginx 1.13.8版本後是這麼獲取的:

+#if (NGX_HAVE_LEVEL1_DCACHE_LINESIZE)
+    size = sysconf(_SC_LEVEL1_DCACHE_LINESIZE);
+    if (size > 0) {
+        ngx_cacheline_size = size;
+    }
+#endif

為什麼需要cpu cache line這個數值呢?因為它對提高效能是有用的!比如nginx中儲存http header的hash表。假設我們的cache size是64位元組,而一個hash bucket是48位元組。假如某一個bucket的起始地址是1F7D030,那麼它佔用的記憶體就從1F7D030到1F7D05F,而cache size的特性導致只會從64的整數倍地址訪問,於是需要訪問兩次:1F7D000和1F7D040。而如果我們能使得hash bucket大小是cache size的整數倍,那麼就不會出現訪問一個hash bucket需要兩次操作主存的情況。比如,若原本bucket size是32,則設為64;原本為96,則設為128,即向上對齊。nginx有一個向上對齊函式就是做這個事的:

#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))

    cmcf->server_names_hash_bucket_size =
            ngx_align(cmcf->server_names_hash_bucket_size, ngx_cacheline_size);

上面這個ngx_align演算法來源於一個數學特性:對於正整數2^n(n>1)來說,存在這樣的特性,如果整數X是2^n的整數倍,則X的二進位制形式的低n位為0, 如果X不是2^n的整數倍,則X與(~(2^n-1))進行與運算可以得到一個與X相近的是2^n整數倍的正整數。對於上對齊,則需要先加上2^n-1,再進行上述運算。

事實上,如果hash bucket沒有對齊cache line,那麼出現訪問一個bucket要呼叫兩次載入主存資料的操作可能性非常大!比如上面的例子中hash bucket size是48,即使第一個bucket沒有跨cache line,第2個bucket一定會跨從而導致兩次主存訪問!

當CPU獲取資料時,cpu快取由於已經存有資料,那麼核心可以直接使用快取,不用再去訪問記憶體了,這一過程我們稱為cache hit命中!反之,稱為cache miss。可見,如果我們的程式在迴圈或者熱點程式碼中,能夠控制資料規模,使之長期落在CPU快取中,那麼效能就可以提升!怎麼判斷CPU快取命中率現在是多少呢?在linux下可以通過perf命令輕鬆實現(centos下通過yum install perf安裝),如下所示:

[[email protected] test]# perf stat -B -e cache-references,cache-misses ./test5 64
time cost: 12283832us

 Performance counter stats for './test5 64':

           440,366      cache-references                                            
           157,177      cache-misses              #   35.692 % of all cache refs    

      12.290852528 seconds time elapsed

當然,perf支援很多事件,包括程序上下文切換等,上面的cache-references,cache-misses兩個事件分別代表快取命中和未命中。perf支援的事件很多,如下表所示:

  branch-instructions OR branches                    [Hardware event]
  branch-misses                                      [Hardware event]
  bus-cycles                                         [Hardware event]
  cache-misses                                       [Hardware event]
  cache-references                                   [Hardware event]
  cpu-cycles OR cycles                               [Hardware event]
  instructions                                       [Hardware event]
  ref-cycles                                         [Hardware event]

  alignment-faults                                   [Software event]
  context-switches OR cs                             [Software event]
  cpu-clock                                          [Software event]
  cpu-migrations OR migrations                       [Software event]
  dummy                                              [Software event]
  emulation-faults                                   [Software event]
  major-faults                                       [Software event]
  minor-faults                                       [Software event]
  page-faults OR faults                              [Software event]
  task-clock                                         [Software event]

  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-icache-load-misses                              [Hardware cache event]
  LLC-load-misses                                    [Hardware cache event]
  LLC-loads                                          [Hardware cache event]
  LLC-store-misses                                   [Hardware cache event]
  LLC-stores                                         [Hardware cache event]
  branch-load-misses                                 [Hardware cache event]
  branch-loads                                       [Hardware cache event]
  dTLB-load-misses                                   [Hardware cache event]
  dTLB-loads                                         [Hardware cache event]
  dTLB-store-misses                                  [Hardware cache event]
  dTLB-stores                                        [Hardware cache event]
  iTLB-load-misses                                   [Hardware cache event]
  iTLB-loads                                         [Hardware cache event]
  node-load-misses                                   [Hardware cache event]
  node-loads                                         [Hardware cache event]
  node-store-misses                                  [Hardware cache event]
  node-stores                                        [Hardware cache event]

使用perf來定位程式效能的瓶頸是個有效的辦法!

下一篇我們來討論怎樣寫出能利用好CPU快取的程式碼。

(轉載本站文章請註明作者和出處 陶輝筆記 ,請勿用於任何商業用途)