perf 性能分析實例——使用perf優化cache利用率


摘要:本文主要講解如何使用perf觀察程序在緩存利用方面的瓶頸,進而優化程序,提高cache命中率。主要講解提高緩存利用的幾種常用方法。

 

1.程序局部性

 

一個編寫良好的計算機程序通常具有程序的局部性,它更傾向於引用最近引用過的數據項,或者這個數據周圍的數據——前者是時間局部性,后者是空間局部性。現代操作系統的設計,從硬件到操作系統再到應用程序都利用了程序的局部性原理:硬件層,通過cache來緩存剛剛使用過的指令或者數據,來提交對內存的訪問效率。在操作系統級別,操作系統利用主存來緩存剛剛訪問過的磁盤塊;在應用層,web瀏覽器將最近引用過的文檔放在磁盤上,大量的web服務器將最近訪問的文檔放在前端磁盤上,這些緩存能夠滿足很多請求而不需要服務器的干預。本文主要將的是硬件層次的程序局部性。

 

2.處理器存儲體系

 

計算機體系的儲存層次從內到外依次是寄存器、cache(從一級、二級到三級)、主存、磁盤、遠程文件系統;從內到外,訪問速度依次降低,存儲容量依次增大。這個層次關系,可以用下面這張圖來表示:

程序在執行過程中,數據最先在磁盤上,然后被取到內存之中,最后如果經過cache(也可以不經過cache)被CPU使用。如果數據不再cache之中,需要CPU到主存中存取數據,那么這就是cache miss,這將帶來相當大的時間開銷。

 

3.perf原理與使用簡介

    Perf是Linux kernel自帶的系統性能優化工具。Perf的優勢在於與linux Kernel的緊密結合,它可以最先應用到加入Kernel的new feature。perf可以用於查看熱點函數,查看cashe miss的比率,從而幫助開發者來優化程序性能。

  性能調優工具如 perf,Oprofile 等的基本原理都是對被監測對象進行采樣,最簡單的情形是根據 tick 中斷進行采樣,即在 tick 中斷內觸發采樣點,在采樣點里判斷程序當時的上下文。假如一個程序 90% 的時間都花費在函數 foo() 上,那么 90% 的采樣點都應該落在函數 foo() 的上下文中。運氣不可捉摸,但我想只要采樣頻率足夠高,采樣時間足夠長,那么以上推論就比較可靠。因此,通過 tick 觸發采樣,我們便可以了解程序中哪些地方最耗時間,從而重點分析。

   本文,我們主要關心的是cache miss事件,那么我們只需要統計程序cache miss的次數即可。使用perf 來檢測程序執行期間由此造成的cache miss的命令是perf stat -e cache-misses ./exefilename,另外,檢測cache miss事件需要取消內核指針的禁用(/proc/sys/kernel/kptr_restrict設置為0)。

 

4.cache 優化實例

 

4.1數據合並

有兩個數據A和B,訪問的時候經常是一起訪問的,總是會先訪問A再訪問B。這樣A[i]和B[i]就距離很遠,如果A、B是兩個長度很大的數組,那么可能A[i]和B[i]無法同時存在cache之中。為了增加程序訪問的局部性,需要將A[i]和B[i]盡量存放在一起。為此,我們可以定義一個結構體,包含A和B的元素各一個。這樣的兩個程序對比如下:

test.c

 

  1. #define NUM 393216  
  2.   
  3. int main(){  
  4.     float a[NUM],b[NUM];  
  5.     int i;  
  6.     for(i=0;i<1000;i++)  
  7.         add(a,b,NUM);  
  8. }  
  9.   
  10. int add(int *a,int *b,int num){  
  11.     int i=0;  
  12.     for(i=0;i<num;i++){  
  13.         *a=*a+*b;  
  14.         a++;  
  15.         b++;  
  16.     }     
  17. }  


test2.c

 

 

  1. #define NUM 39216  
  2. typedef struct{  
  3.     float a;  
  4.     float b;  
  5. }Array;  
  6.   
  7. int main(){  
  8.     Array myarray[NUM];  
  9.     int j=0;  
  10.     for(j=0;j<1000;j++)  
  11.         add(myarray,NUM);  
  12. }  
  13.   
  14. int add(Array *myarray,int num){  
  15.     int i=0;  
  16.     for(i=0;i<num;i++){  
  17.         myarray->a=myarray->a+myarray->b;  
  18.         myarray++;  
  19.     }     
  20. }  

 

 

注:我們設置數組大小NUM為39216,因為cache大小是3072KB,這樣設定可以讓A數組填滿cache,方便對比。

對比二者的cache miss數量:

test

 

  1. [huangyk@huangyk test]$ perf stat -e cache-misses ./test  
  2.   
  3.   
  4.  Performance counter stats for './test':  
  5.   
  6.   
  7.            530,787 cache-misses                                                  
  8.   
  9.   
  10.        2.372003220 seconds time elapsed  

test2

 

 

  1. [huangyk@huangyk test]$ perf stat -e cache-misses ./test2  
  2.   
  3.  Performance counter stats for './test2':  
  4.   
  5.             11,636 cache-misses                                                  
  6.   
  7.        0.233570690 seconds time elapsed  


可以看到,后者的cache miss數量相對前者有很大的下降,耗費的時間大概是前者的十分之一左右。

進一步,可以查看觸發cach-miss的函數:

test程序的結果:

 

  1. [huangyk@huangyk test]$ perf record -e cache-misses ./test  
  2. [huangyk@huangyk test]$ perf report  
  3. Samples: 7K of event 'cache-misses', Event count (approx.): 3393820  
  4.  45.88%  test  test               [.] sub  
  5.  44.74%  test  test               [.] add  
  6.   0.71%  test  [kernel.kallsyms]  [k] clear_page_c  
  7.   0.70%  test  [kernel.kallsyms]  [k] _spin_lock  
  8.   0.43%  test  [kernel.kallsyms]  [k] run_timer_softirq  
  9.   0.41%  test  [kernel.kallsyms]  [k] account_user_time  
  10.   0.34%  test  [kernel.kallsyms]  [k] hrtimer_interrupt  
  11.   0.30%  test  [kernel.kallsyms]  [k] run_posix_cpu_timers  
  12.   0.29%  test  [kernel.kallsyms]  [k] _cond_resched  
  13.   0.24%  test  [kernel.kallsyms]  [k] update_curr  
  14.   0.23%  test  [kernel.kallsyms]  [k] x86_pmu_disable  
  15.   0.22%  test  [kernel.kallsyms]  [k] __rcu_pending  
  16.   0.20%  test  [kernel.kallsyms]  [k] task_tick_fair  
  17.   0.19%  test  [kernel.kallsyms]  [k] account_process_tick  
  18.   0.17%  test  [kernel.kallsyms]  [k] scheduler_tick  
  19.   0.16%  test  [kernel.kallsyms]  [k] __perf_event_task_sched_out  


從perf輸出的結果可以看出,程序cache miss主要是由sub和add觸發的。

 

test2程序的結果:

  1. perf record -e cache-misses ./test2  
  2. perf report  
  3. Samples: 51  of event 'cache-misses', Event count (approx.): 17438  
  4.  39.78%  test2  [kernel.kallsyms]  [k] clear_page_c  
  5.  15.68%  test2  [kernel.kallsyms]  [k] mem_cgroup_uncharge_start  
  6.   7.94%  test2  [kernel.kallsyms]  [k] __alloc_pages_nodemask  
  7.   7.28%  test2  [kernel.kallsyms]  [k] init_fpu  
  8.   6.50%  test2  test2              [.] add  
  9.   3.19%  test2  [kernel.kallsyms]  [k] arp_process  
  10.   2.76%  test2  [kernel.kallsyms]  [k] account_user_time  
  11.   2.73%  test2  [kernel.kallsyms]  [k] perf_event_mmap  
  12.   2.02%  test2  [kernel.kallsyms]  [k] filemap_fault  
  13.   1.62%  test2  [kernel.kallsyms]  [k] kfree  
  14.   1.50%  test2  [kernel.kallsyms]  [k] 0xffffffffa04334b8  
  15.   1.06%  test2  [kernel.kallsyms]  [k] _spin_lock  
  16.   1.06%  test2  [kernel.kallsyms]  [k] raise_softirq  
  17.   1.00%  test2  [kernel.kallsyms]  [k] acct_update_integrals  
  18.   0.96%  test2  [kernel.kallsyms]  [k] __do_softirq  
  19.   0.67%  test2  [kernel.kallsyms]  [k] handle_edge_irq  
  20.   0.56%  test2  [kernel.kallsyms]  [k] __rcu_pending  
  21.   0.40%  test2  [kernel.kallsyms]  [k] enqueue_hrtimer  
  22.   0.39%  test2  test2              [.] sub  
  23.   0.38%  test2  [kernel.kallsyms]  [k] _spin_lock_irq  
  24.   0.36%  test2  [kernel.kallsyms]  [k] tick_sched_timer  





從perf輸出的結果可以看出,add和sub觸發的perf miss已經占了很小的一部分。


4.2循環交換



C語言中,對於二維數組,同一行的數據是相鄰的,同一列的數據是不相鄰的。如果在循環中,讓依次訪問的數據盡量處在內存中相鄰的位置,那么程序的局部性將會得到很大的提高。
觀察下面矩陣相乘的幾個函數:
test_ijk:

  1. void Muti( double A[][NUM],double B[][NUM],double C[][NUM],int n){   
  2.     int i,j,k;  
  3.     double sum=0;  
  4.     for (i=0;i<n;i++)  
  5.         for(j=0;j<n;j++){  
  6.             sum=0.0;  
  7.             for(k=0;k<n;k++)  
  8.                 sum+=A[i][k]*B[k][j];  
  9.             C[i][j]+=sum;  
  10.         }     


test_jki:

  1. void Muti( double A[][NUM],double B[][NUM],double C[][NUM],int n){   
  2.     int i,j,k;  
  3.     double sum=0;  
  4.     for (j=0;j<n;j++)  
  5.         for(k=0;k<n;k++){  
  6.             sum=B[k][j];  
  7.             for(i=0;i<n;i++)  
  8.                 C[i][j]+=A[i][k]*sum;  
  9.         }         
  10. }  


test_kij:

  1. void Muti( double A[][NUM],double B[][NUM],double C[][NUM],int n){   
  2.     int i,j,k;  
  3.     double sum=0;  
  4.     for (k=0;k<n;k++)  
  5.         for(i=0;i<n;i++){  
  6.             sum=A[i][k];  
  7.             for(j=0;j<n;j++)  
  8.                 C[i][j]+=B[k][j]*sum;  
  9.         }         
  10. }  


考察內層循環,可以發現,不同的循環模式,導致的cache失效比例依次是kij、ijk、jki遞增。


4.3循環合並



在很多情況下,我們可能使用兩個獨立的循環來訪問數組a和c。由於數組很大,在第二個循環訪問數組中元素的時候,第一個循環取進cache中的數據已經被替換出去,從而導致cache失效。如此情況下,可以將兩個循環合並在一起。合並以后,每個數組元組在同一個循環體中被訪問了兩次,從而提高了程序的局部性。


5.后記





實際情況下,一個程序的cache失效比例往往並不像我們從理論上預測的那么簡單。影響cache失效比例的因素主要有:數組大小,cache映射策略,二級cache大小,Victim Cache等,同時由於cache的不同寫回策略,我們也很難從理論上預估一個程序由於cache miss而導致的時間耗費。真正在進行程序設計的時候,我們在進行理論上的分析之后,只有使用perf等性能調優工具,才能更真實地觀察到程序對cache的利用情況。


免責聲明!

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



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