OProfile for Linux on POWER 識別性能瓶頸

發表于:2007-05-25來源:作者:點擊數: 標簽:
級別: 初級 John Engel Linux 技術顧問, IBM 2005 年 6 月 27 日 了解關于 OProfile 的知識,學習如何在基于 IBM POWER 處理器、運行 Linux 的 服務器 上使用它。首先,閱讀關于 OProfile 的概述,并了解它在 Linux on POWER 上的實現,然后跟隨作者給出的兩

級別: 初級

John Engel
Linux 技術顧問, IBM
2005 年 6 月 27 日

了解關于 OProfile 的知識,學習如何在基于 IBM® POWER™ 處理器、運行 Linux™ 的服務器上使用它。首先,閱讀關于 OProfile 的概述,并了解它在 Linux on POWER 上的實現,然后跟隨作者給出的兩個例子,學習如何在 Linux on POWER 平臺上使用 OProfile 分析代碼和結果。

簡介
作為一名開發人員,在試圖提高代碼效率時,您可能發現性能瓶頸是您要面對的最困難的任務之一。代碼分析(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 守護進程收集樣本數據??刂圃撌刈o進程的工具稱作 opcontrol。表 1 列出了用于 opcontrol 的一些常見的命令行選項。本文的后面還將描述 opreport 和 opannotate 這兩個工具,它們都是用于分析收集到的數據的工具。在 OProfile 手冊的第 2.2 節中,可以找到對所有 OProfile 工具的概述。(請參閱參考資料。)

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

表 1. opcontrol 命令行選項

opcontrol 選項 描述
--list-events 列出處理器事件和單元屏蔽(unit mask)
--vmlinux=<kernel image> 將要分析的內核鏡像文件
--no-vmlinux 不分析內核
--reset 清除當前會話中的數據
--setup 在運行守護進程之前對其進行設置
--event=<processor 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 必須花費大量步驟從主存中檢索數據,這需要占用額外的處理器周期。圖 1 展示了兩個處理器,它們在各自的 L2 緩存中包含一個共享數據結構的一個副本。

圖 1. 共享一個數據結構的兩個處理器

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

清單 4. 共享的數據結構

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

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

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

            #include <stdlib.h>
            #include <sched.h>
            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;
            

圖 2 展示了在填充數據結構后,如何使得每個處理器上的每個數據元素都存儲在各自的緩存行中。

圖 2. 共享填充后的數據結構的兩個處理器

像前面那樣重新編譯該程序,但是這一次使用修改后的數據結構。然后使用 清單 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 <stdlib.h>
            #include <sched.h>
            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。

致謝
我要感謝 Linda Kinnunen,是她提供了文檔模板并對本文進行了審校,我還要感謝 Maynard Johnson 對本文進行了技術上的審校。

原文轉自:http://www.anti-gravitydesign.com

国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97