用profile和oprofile監視視linux性能

profile使用:
profile功能是架構無關的,能夠用來監視linux內核的4項功能,即:
 11 #define CPU_PROFILING   1
 12 #define SCHED_PROFILING 2
 13 #define SLEEP_PROFILING 3
 14 #define KVM_PROFILING   4
要想找開profile功能,除了要在menuconfig中打開支持選項外,還要在命令行加上profile=**,##.
**表示上述4種功能之一,##表示一個數字,用來表示監視的顆粒度,越小越細。
當作了這些工做以後還須要用到util linux工具中的readprofile來讀取結果,結果是從/proc/profile文件中讀取的,此工具作了格式化處理。如下爲轉載:
1. 如何使用profile:
首先確認內核支持profile,而後在內核啓動時加入如下參數:profile=1或者其它參數, 新的內核支持profile=schedule,1
2. 內核啓動後會建立/proc/profile文件,這個文件能夠經過readprofile讀取,
如readprofile -m /proc/kallsyms | sort -nr > ~/cur_profile.log,
或者readprofile -r -m /proc/kallsyms |sort -nr,
或者readprofile -r && sleep 1 && readprofile -m /proc/kallsyms |sort -nr >~/cur_profile.log
3. 讀取/proc/profile可得到哪些內容?
根據啓動配置profile=?的不一樣,獲取的內容不一樣:
若是設置成profile=schedule能夠得到每一個函數調用schedule的次數,用來調試schedule頗有用
profile的實現:
在內核中建立一個/proc/profile接口,在系統啓動時用profile_init()分配好存放profile信息的內存,每條指令都有一個計數器。
若是設置的是profile=2 統計每條指令執行的次數。在時鐘中斷中調用        profile_tick(CPU_PROFILING, regs),將當前指令regs->eip的計數值+1。這個統計有點不許,由於一個jiffies之間,可能執行不少函數,而統計的只是剛好發生 時鐘中斷時的那個函數。但取樣點多了,這些信息仍是能說明問題。
若是設置的是profile=schedule 統計每一個指令調用schedule()的次數,在schedule()中調用profile_hit(SCHED_PROFILING, __builtin_return_address(0));
其實真正調用schedule的指令只有有限的幾個,但這些信息能夠得到調度點的精確信息。

profile_hit()的做用是將當前指令的計數值加1
profile_tick()是在每一個時鐘tick的時候將響應的指令計數值加1
time_hook 通常被其它profile工具,如oprofile用來在每次中斷髮生時,添加本身的處理函數。

profile信息其實包括任務的全部統計信息,因此能夠用profile_event_register()在任務退出或者用戶空間內存釋放時,掛載本身的回調函數,以統計這些信息。

profile信息的統計在smp和up下不一樣,即profile_hit的實現不一樣,smp的實現中有一個PerCPU cache,這可避免多個CPU在profile統計時效率低下問題。具體能夠察看源代碼kernel/profile.c
oprofile使用:
oprofile平臺相關工具,請注意本身平臺支持的event.

http://oprofile.sourceforge.net/doc/index.html
http://oprofile.sourceforge.net/doc/internals/index.html

簡介

做爲一名開發人員,在試圖提升代碼效率時,您可能發現性能瓶頸是您要面對的最困難的任務之一。代碼分析(code profiling)是一種可使這項任務變得更容易的方法。代碼分析包括對那些表示運行系統上的某些處理器活動的數據樣本進行分析。OProfile 爲 POWER 上的 Linux 提供了這種解決方案。OProfile 被包含在最新的 IBM? 支持的 Linux for POWER 發行版本中:Red Hat Enterprise Linux 4 (RHEL4) 和 SUSE LINUX Enterprise Server 9 (SLES9)。本文將介紹 OProfile for Linux on POWER,並提供兩個例子,演示如何使用它來發現性能瓶頸。


代碼分析概述

OProfile for Linux on POWER 使用了一個內核模塊和一個用戶空間守護進程,前者能夠訪問性能計數寄存器,後者在後臺運行,負責從這些寄存器中收集數據。在啓動守護進程以前,OProfile 將配置事件類型以及每種事件的樣本計數(sample count)。若是沒有配置任何事件,那麼 OProfile 將使用 Linux on POWER 上的默認事件,即 CYCLES,該事件將對處理器循環進行計數。事件的樣本計數將決定事件每發生多少次計數器才增長一次。OProfile 被設計成能夠在低開銷下運行,從而使後臺運行的守護進程不會擾亂系統性能。

OProfile 具備對 POWER4?、POWER5? 和 PowerPC? 970 處理器的內核支持。PowerPC 970 和 POWER4 處理器有 8 個計數寄存器,而 POWER5 處理器有 6 個計數寄存器。在不具有 OProfile 內核支持的架構上使用的則是計時器(timer)模式。在這種模式下,OProfile 使用了一個計數器中斷,對於禁用中斷的代碼,OProfile 不能對其進行分析。

OProfile 工具

與 OProfile 內核支持一塊兒提供的還有一些與內核交互的用戶空間工具,以及分析收集到的數據的工具。如前所述,OProfile 守護進程收集樣本數據。控制該守護進程的工具稱做 opcontrol。表 1 列出了用於 opcontrol 的一些常見的命令行選項。本文的後面還將描述 opreport 和 opannotate 這兩個工具,它們都是用於分析收集到的數據的工具。在 OProfile 手冊的第 2.2 節中,能夠找到對全部 OProfile 工具的概述。(請參閱參考資料。)

RHEL4 和 SLES9 上支持的處理器事件類型是不一樣的,正如不一樣 POWER 處理器上支持的事件類型也會有所變化同樣。您可使用 opcontrol 工具和 --list-events 選項得到本身平臺所支持的那些事件的列表。

表 1. opcontrol 命令行選項
opcontrol 選項    描述
--list-events    列出處理器事件和單元屏蔽(unit mask)
--vmlinux=    將要分析的內核鏡像文件
--no-vmlinux    不分析內核
--reset    清除當前會話中的數據
--setup    在運行守護進程以前對其進行設置
--event=    監視給定的處理器事件
--start    開始取樣
--dump    使數據流到守護進程中
--stop    中止數據取樣
-h    關閉守護進程

OProfile 例子

您可使用 OProfile 來分析處理器週期、TLB 失誤、內存引用、分支預測失誤、緩存失誤、中斷處理程序,等等。一樣,您可使用 opcontrol 的 --list-events 選項來提供完整的特定處理器上可監視事件列表。

下面的例子演示瞭如何使用 OProfile for Linux on POWER。第一個例子監視處理器週期,以發現編寫不當、會致使潛在性能瓶頸的算法。雖然這是一個很小的例子,可是當您分析一個應用程序,指望發現大部分處理器週期究竟用在什麼地方時,仍能夠借鑑這裏的方法。而後您能夠進一步分析這部分代碼,看是否能夠對其進行優化。

第二個例子要更爲複雜一些 —— 它演示瞭如何發現二級(level 2,L2)數據緩存失誤,併爲減小數據緩存失誤的次數提供了兩套解決方案。

例 1: 分析編寫不當的代碼

這個例子的目的是展現如何編譯和分析一個編寫不當的代碼示例,以分析哪一個函數性能不佳。這是一個很小的例子,只包含兩個函數 —— slow_multiply() 和 fast_multiply() —— 這兩個函數都是用於求兩個數的乘積,以下面的清單 1 所示。

清單 1. 兩個執行乘法的函數

 
int fast_multiply(x,  y)
{
        return x * y;
}
int slow_multiply(x, y)
{
        int i, j, z;
        for (i = 0, z = 0; i < x; i++)
                z = z + y;
        return z;
}
int main()
{
        int i,j;
        int x,y;
        for (i = 0; i < 200; i ++) {
                for (j = 0; j " 30 ; j++) {
                        x = fast_multiply(i, j);
                        y = slow_multiply(i, j);
                }
        }
        return 0;
}


分析這個代碼,並使用 opannotate 對其進行分析,該工具使您能夠用 OProfile 註釋查看源代碼。首先必須利用調試信息來編譯源代碼,opannotate 要用它來添加註釋。使用 Gnu Compiler Collections C 編譯器,即 gcc,經過運行如下命令來編譯清單 1 中的例子。注意,-g 標誌意味着要添加調試信息。

 gcc  -g multiply.c -o multiply   


接下來,使用 清單 2 中的命令分析該代碼,而後使用 CYCLES 事件計算處理器週期,以分析結果。

清單 2. 用來分析乘法例子的命令

# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup --event=CYCLES:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./multiply
# opcontrol --dump
# opcontrol --stop
Stopping profiling.
# opcontrol -h
Stopping profiling.
Killing daemon.


最後,使用 opannotate 工具和 --source 選項生成源代碼,或者和 --assembly 選項一塊兒生成彙編代碼。具體使用這兩個選項中的哪個選項,或者是否同時使用這兩個選項,則取決於您想要分析的詳細程度。對於這個例子,只需使用 --source 選項來肯定大部分處理器週期發生在什麼地方便可。

清單 3. 對乘法例子的 opannotate 結果的分析

# opannotate --source ./multiply
/*
 * Command line: opannotate --source ./multiply
 *
 * Interpretation of command line:
 * Output annotated source file with samples
 * Output all files
 *
 * CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
 * Counted CYCLES events (Processor cycles) with a unit mask of
0x00 (No unit mask) count 1000
 */
/*
 * Total samples for file : "/usr/local/src/badcode/multiply.c"
 *
 *   6244 100.000
 */
               :int fast_multiply(x, y)
    36  0.5766 :{ /* fast_multiply total:     79  1.2652 */
    26  0.4164 :        return x * y;
    17  0.2723 :}
               :
               :int slow_multiply(x, y)
    50  0.8008 :{ /* slow_multiply total:   6065 97.1332 */
               :        int i, j, z;
  2305 36.9154 :        for (i = 0, z = 0; i " x; i++)
  3684 59.0006 :                z = z + y;
    11  0.1762 :        return z;
    15  0.2402 :}
               :
               :int main()
               :{ /* main total:    100  1.6015 */
               :        int i,j;
               :        int x,y;
               :
     1  0.0160 :        for (i = 0; i " 200; i ++) {
     6  0.0961 :                for (j = 0; j " 30 ; j++) {
    75  1.2012 :                        x = fast_multiply(i, j);
    18  0.2883 :                        y = slow_multiply(i, j);
               :                }
               :        }
               :        return 0;
               :}
               


清單 3 中下面的幾行將顯示兩個乘法函數中所使用的 CYCLES 數:

36  0.5766 :{ /* fast_multiply total:     79  1.2652 */


50  0.8008 :{ /* slow_multiply total:   6065 97.1332 */


您能夠看到,fast_mulitply() 只使用了 79 個樣本,而 slow_multiply() 使用了 6065 個樣本。雖然這是一個很小的例子,在現實中不大可能出現,但它仍然足以演示如何剖析代碼,併爲發現性能瓶頸而對其進行分析。

例 2:發現二級數據緩存失誤

這個例子比第一個例子要複雜一些,它須要發現二級(L2)數據緩存失誤。POWER 處理器包含芯片二級緩存(on-chip L2 cache),這是鄰近處理器的一種高速存儲器。處理器從 L2 緩存中訪問常常修改的數據。當兩個處理器共享一個數據結構,並同時修改那個數據結構時,就有可能引起問題。CPU1 在它的 L2 緩存中包含數據的一個副本,而 CPU2 修改了這個共享的數據結構。CPU1 L2 緩存中的副本如今是無效的,必須進行更新。CPU1 必須花費大量步驟從主存中檢索數據,這須要佔用額外的處理器週期。

 
在這個例子中,您將查看這個數據結構(如清單 4 所示),並分析兩個處理器同時修改這個數據結構時出現的情景)。而後觀察數據緩存失誤,並考察用來修正這個問題的兩種解決方案。

清單 4. 共享的數據結構

struct shared_data_struct {
   unsigned int data1;
   unsigned int data1;
}


清單 5 中的程序使用 clone() 系統調用和 VM_CLONE 標誌生成一個子進程。VM_CLONE 標誌會致使子進程和父進程在同一個存儲空間中運行。父線程修改該數據結構的第一個元素,而子線程則修改第二個元素。

清單 5. 演示 L2 數據緩存失誤的代碼示例

#include
#include
struct shared_data_struct {
        unsigned int data1;
        unsigned int data2;
};
struct shared_data_struct shared_data;
static int inc_second(struct shared_data_struct *);
int main(){
        int i, j, pid;
        void *child_stack;
        /* allocate memory for other process to execute in */
        if((child_stack = (void *) malloc(4096)) == NULL) {
                perror("Cannot allocate stack for child");
                exit(1);
        }
        /* clone process and run in the same memory space */
        if ((pid = clone((void *)&inc_second, child_stack,
           CLONE_VM, &shared_data)) < 0) {
                perror("clone called failed.");
                exit(1);
        }
        /* increment first member of shared struct */
        for (j = 0; j < 2000; j++) {
                for (i = 0; i < 100000; i++) {
                        shared_data.data1++;
                }
        }
        return 0;
}
int inc_second(struct shared_data_struct *sd)
{
        int i,j;
        /* increment second member of shared struct */
        for (j = 1; j < 2000; j++) {
                for (i = 1; i < 100000; i++) {
                        sd->data2++;
                }
        }
}


使用 gcc 編譯器,運行清單 6 中的命令不帶優化地編譯這個示例程序。

清單 6. 用於編譯清單 5 中例子代碼的命令

gcc -o cache-miss cache-miss.c


如今您能夠用 OProfile 分析上述程序中出現的 L2 數據緩存失誤。

對於這個例子,做者在一臺 IBM eServer? OpenPower? 710 上執行和分析了這個程序,該機器有兩個 POWER5 處理器,並運行 SLES9 Service Pack 1 (SLES9SP1)。將 --list-events 標誌傳遞給 opcontrol,以判斷是哪個事件負責監視 L2 數據緩存失誤。對於基於 POWER5 處理器的、運行 SLES9SP1 的系統,由 PM_LSU_LMQ_LHR_MERGE_GP9 事件監視 L2 數據緩存失誤。若是您將樣本計數設置爲 1000,好比在這個例子中,那麼 OProfile 將從每 1000 個硬件事件抽取一個樣本。若是使用不一樣的平臺,例如基於 POWER4 處理器的服務器,那麼這樣的事件也會有所不一樣。

使用 清單 7 中的命令分析這個例子代碼,以下所示:

清單 7. 用來分析清單 5 所示例子中的 L2 數據緩存失誤的命令

# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=PM_LSU_LMQ_LHR_MERGE_GP9:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
Counted PM_LSU_LMQ_LHR_MERGE_GP9 events (Dcache miss occurred for
  the same real cache line as earlier req, merged into LMQ) with a
    unit mask of 0x00 (No unit mask) count 1000
samples  %        symbol name
47897    58.7470  main
33634    41.2530  inc_second


在分析來自 opreport 的結果時,您能夠看到,在函數 main() 和 inc_second() 中存在不少緩存失誤。opreport 的 -l 選項將輸出符號信息,而實質上輸出的應該只是二進制映像名。一樣,緩存失誤的原由也是兩個處理器修改一個共享的數據結構,這個數據結構大小爲 8 字節,放在一個 128 字節的緩存行中。

消除數據緩存失誤的一種方法是填充數據結構,使得它的每個元素都存儲在各自的緩存行中。清單 8 包含一個修改後的結構,其中有 124 字節的填充物。

清單 8. 帶填充物的數據結構,每一個元素放進不一樣的緩存行中

struct shared_data_struct {
   unsigned int data1;
   char pad[124];
   unsigned int data1;



像前面那樣從新編譯該程序,可是這一次使用修改後的數據結構。而後使用 清單 9 中的命令再次分析結果。

清單 9. 填充數據結構後用於 profile L2 數據緩存失誤的命令

# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=PM_LSU_LMQ_LHR_MERGE_GP9:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
error: no sample files found: profile specification too strict ?


Opreport 代表,因爲沒有發現抽樣數據,因此可能存在錯誤。然而,隨着對共享數據結構的修改,這是能夠預期的,由於每一個數據元素都在本身的緩存行中,因此不存在 L2 緩存失誤。

如今能夠考察 L2 緩存失誤在處理器週期上的代價。首先,分析使用未填充的原有共享數據結構的代碼(清單 4)。您將進行抽樣的事件是 CYCLES。使用 清單 10 中的命令針對 CYCLES 事件分析這個例子。

清單 10. 用於 profile 清單 5 所示例子中處理器週期數的命令

# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=CYCLES:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
Counted CYCLES events (Processor cycles) with a unit mask of 0x00
(No unit mask) count 1000
samples  %        symbol name
121166   53.3853  inc_second
105799   46.6147  main
 


如今,使用 清單 11 中的命令分析使用填充後的數據結構的例子代碼(清單 8)。

清單 11. 用於分析使用填充後的數據結構的例子中處理器週期數的命令

# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=CYCLES:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
Counted CYCLES events (Processor cycles) with a unit mask of 0x00
 (No unit mask) count 1000
samples  %        symbol name
104916   58.3872  inc_second
74774    41.6128  main


不出所料,隨着 L2 緩存失誤數量的增長,處理器週期數也有所增長。其主要緣由是,與從 L2 緩存取數據相比,從主存獲取數據代價昂貴。

避免兩個處理器之間緩存失誤的另外一種方法是在相同處理器上運行兩個線程。經過使用 Cpu 類似性(affinity),將一個進程綁定到一個特定的處理器,下面的例子演示了這一點。在 Linux 上,sched_setaffinity() 系統調用在一個處理器上運行兩個線程。 清單 12 提供了原來的示例程序的另外一個變體,其中使用 sched_setaffinity() 調用來執行這一操做。

清單 12. 利用 cpu 類似性來避免 L2 緩存失誤的示例代碼

#include
#include
struct shared_data_struct {
        unsigned int data1;
        unsigned int data2;
};
struct shared_data_struct shared_data;
static int inc_second(struct shared_data_struct *);
int main(){
        int i, j, pid;
        cpu_set_t cmask;
        unsigned long len = sizeof(cmask);
        pid_t p = 0;
        void *child_stack;
        __CPU_ZERO(&cmask);
        __CPU_SET(0, &cmask);
        /* allocate memory for other process to execute in */
        if((child_stack = (void *) malloc(4096)) == NULL) {
                perror("Cannot allocate stack for child");
                exit(1);
        }
        /* clone process and run in the same memory space */
        if ((pid = clone((void *)&inc_second, child_stack,
CLONE_VM, &shared_data)) < 0) {
                perror("clone called failed");
                exit(1);
        }
        if (!sched_setaffinity(0, len, &cmask)) {
                printf("Could not set cpu affinity for current
process.\n");
                exit(1);
        }
        if (!sched_setaffinity(pid, len, &cmask)) {
                printf("Could not set cpu affinity for cloned
process.\n");
                exit(1);
        }
        /* increment first member of shared struct */
        for (j = 0; j < 2000; j++) {
                for (i = 0; i < 100000; i++) {
                        shared_data.data1++;
                }
        }
        return 0;
}
int inc_second(struct shared_data_struct *sd)
{
        int i,j;
        /* increment second member of shared struct */
        for (j = 1; j < 2000; j++) {
                for (i = 1; i < 100000; i++) {
                        sd->data2++;
                }
        }
}


這個例子在同處理器上運行兩個線程,共享數據結構存放在一個處理器上的一個 L2 緩存行中。這樣應該能夠致使零緩存失誤。使用前面描述的步驟分析緩存失誤,以驗證在一個處理器上運行兩個進程時,是否不存在 L2 緩存失誤。對於數據緩存失誤這個問題,第三種解決方法是使用編譯器優化,這樣能夠減小緩存失誤的數量。然而,在某些環境下,這不是一個合適的選擇,您仍然必須分析代碼,並對不良性能作出改正。



結束語

分析是開發過程當中最困難的任務之一。爲了使代碼得到最佳性能,好的工具是必不可少的。OProfile 就是這樣一種工具,目前它提供了針對 Linux on POWER 的分析功能。對於其餘平臺上的能夠快速移植到 Linux on POWER 的 Linux,還有其餘許多性能和調試工具。除了處理器事件的類型有所差異外,在基於 POWER 處理器的 Linux 平臺上運行 OProfile 與在其餘架構上運行 OProfile 是相似的。因此,若是在其餘平臺上使用過 OProfile,那麼您應該在很短期內就能夠知道如何在 Linux on POWER 上運行 OProfile。html

相關文章
相關標籤/搜索