了解關(guān)于 OProfile 的知識,學習如何在基于 IBM® POWER™ 處理器、運行 Linux™ 的服務器上使用它。首先,閱讀關(guān)于 OProfile 的概述,并了解它在 Linux on POWER 上的實現(xiàn),然后跟隨作者給出的兩個例子,學習如何在 Linux on POWER 平臺上使用 OProfile 分析代碼和結(jié)果。
簡介
作為一名開發(fā)人員,在試圖提高代碼效率時,您可能發(fā)現(xiàn)性能瓶頸是您要面對的最困難的任務之一。代碼分析(code profiling)是一種可以使這項任務變得更容易的方法。代碼分析包括對那些表示運行系統(tǒng)上的某些處理器活動的數(shù)據(jù)樣本進行分析。OProfile 為 POWER 上的 Linux 提供了這種解決方案。OProfile 被包含在最新的 IBM® 支持的 Linux for POWER 發(fā)行版本中:Red Hat Enterprise Linux 4 (RHEL4) 和 SUSE LINUX Enterprise Server 9 (SLES9)。本文將介紹 OProfile for Linux on POWER,并提供兩個例子,演示如何使用它來發(fā)現(xiàn)性能瓶頸。
代碼分析概述
OProfile for Linux on POWER 使用了一個內(nèi)核模塊和一個用戶空間守護進程,前者可以訪問性能計數(shù)寄存器,后者在后臺運行,負責從這些寄存器中收集數(shù)據(jù)。在啟動守護進程之前, OProfile 將配置事件類型以及每種事件的樣本計數(shù)(sample count)。如果沒有配置任何事件,那么 OProfile 將使用 Linux on POWER 上的默認事件,即 CYCLES,該事件將對處理器循環(huán)進行計數(shù)。事件的樣本計數(shù)將決定事件每發(fā)生多少次計數(shù)器才增加一次。OProfile 被設計成可以在低開銷下運行,從而使后臺運行的守護進程不會擾亂系統(tǒng)性能。
OProfile 具有對 POWER4™、POWER5™ 和 PowerPC® 970 處理器的內(nèi)核支持。PowerPC 970 和 POWER4 處理器有 8 個計數(shù)寄存器,而 POWER5 處理器有 6 個計數(shù)寄存器。在不具備 OProfile 內(nèi)核支持的架構(gòu)上使用的則是計時器(timer)模式。在這種模式下,OProfile 使用了一個計數(shù)器中斷,對于禁用中斷的代碼,OProfile 不能對其進行分析。
OProfile 工具
與 OProfile 內(nèi)核支持一起提供的還有一些與內(nèi)核交互的用戶空間工具,以及分析收集到的數(shù)據(jù)的工具。如前所述,OProfile 守護進程收集樣本數(shù)據(jù)?刂圃撌刈o進程的工具稱作 opcontrol。表 1 列出了用于 opcontrol 的一些常見的命令行選項。本文的后面還將描述 opreport 和 opannotate 這兩個工具,它們都是用于分析收集到的數(shù)據(jù)的工具。在 OProfile 手冊的第 2.2 節(jié)中,可以找到對所有 OProfile 工具的概述。(請參閱參考資料。)
RHEL4 和 SLES9 上支持的處理器事件類型是不同的,正如不同 POWER 處理器上支持的事件類型也會有所變化一樣。您可以使用 opcontrol 工具和 --list-events 選項獲得自己平臺所支持的那些事件的列表。
表 1. opcontrol 命令行選項
| opcontrol 選項 | 描述 |
| --list-events | 列出處理器事件和單元屏蔽(unit mask) |
| --vmlinux=<kernel image> | 將要分析的內(nèi)核鏡像文件 |
| --no-vmlinux | 不分析內(nèi)核 |
| --reset | 清除當前會話中的數(shù)據(jù) |
| --setup | 在運行守護進程之前對其進行設置 |
| --event=<processor event> | 監(jiān)視給定的處理器事件 |
| --start | 開始取樣 |
| --dump | 使數(shù)據(jù)流到守護進程中 |
| --stop | 停止數(shù)據(jù)取樣 |
| -h | 關(guān)閉守護進程 |
OProfile 例子
您可以使用 OProfile 來分析處理器周期、TLB 失誤、內(nèi)存引用、分支預測失誤、緩存失誤、中斷處理程序,等等。同樣,您可以使用 opcontrol 的 --list-events 選項來提供完整的特定處理器上可監(jiān)視事件列表。
下面的例子演示了如何使用 OProfile for Linux on POWER。第一個例子監(jiān)視處理器周期,以發(fā)現(xiàn)編寫不當、會導致潛在性能瓶頸的算法。雖然這是一個很小的例子,但是當您分析一個應用程序,期望發(fā)現(xiàn)大部分處理器周期究竟用在什么地方時,仍可以借鑒這里的方法。然后您可以進一步分析這部分代碼,看是否可以對其進行優(yōu)化。
第二個例子要更為復雜一些 —— 它演示了如何發(fā)現(xiàn)二級(level 2,L2)數(shù)據(jù)緩存失誤,并為減少數(shù)據(jù)緩存失誤的次數(shù)提供了兩套解決方案。
例 1: 分析編寫不當?shù)拇a
這個例子的目的是展示如何編譯和分析一個編寫不當?shù)拇a示例,以分析哪個函數(shù)性能不佳。這是一個很小的例子,只包含兩個函數(shù) —— slow_multiply() 和 fast_multiply() —— 這兩個函數(shù)都是用于求兩個數(shù)的乘積,如下面的清單 1 所示。
清單 1. 兩個執(zhí)行乘法的函數(shù)
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 注釋查看源代碼。首先必須利用調(diào)試信息來編譯源代碼,opannotate 要用它來添加注釋。使用 Gnu Compiler Collections C 編譯器,即 gcc,通過運行以下命令來編譯清單 1 中的例子。注意,-g 標志意味著要添加調(diào)試信息。
gcc -g multiply.c -o multiply
|
接下來,使用 清單 2 中的命令分析該代碼,然后使用 CYCLES 事件計算處理器周期,以分析結(jié)果。
清單 2. 用來分析乘法例子的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64# opcontrol --reset# opcontrol --setup --event=CYCLES:1000# opcontrol --startUsing 2.6+ OProfile kernel interface.Reading module info.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./multiply# opcontrol --dump# opcontrol --stopStopping profiling.# opcontrol -hStopping profiling.Killing daemon.
|
最后,使用 opannotate 工具和 --source 選項生成源代碼,或者和 --assembly 選項一起生成匯編代碼。具體使用這兩個選項中的哪一個選項,或者是否同時使用這兩個選項,則取決于您想要分析的詳細程度。對于這個例子,只需使用 --source 選項來確定大部分處理器周期發(fā)生在什么地方即可。
清單 3. 對乘法例子的 opannotate 結(jié)果的分析
# 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 of0x00 (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 中下面的幾行將顯示兩個乘法函數(shù)中所使用的 CYCLES 數(shù):
36 0.5766 :{ /* fast_multiply total: 79 1.2652 */
|
50 0.8008 :{ /* slow_multiply total: 6065 97.1332 */
|
您可以看到,fast_mulitply() 只使用了 79 個樣本,而 slow_multiply() 使用了 6065 個樣本。雖然這是一個很小的例子,在現(xiàn)實中不大可能出現(xiàn),但它仍然足以演示如何剖析代碼,并為發(fā)現(xiàn)性能瓶頸而對其進行分析。
例 2:發(fā)現(xiàn)二級數(shù)據(jù)緩存失誤
這個例子比第一個例子要復雜一些,它需要發(fā)現(xiàn)二級(L2)數(shù)據(jù)緩存失誤。POWER 處理器包含芯片二級緩存(on-chip L2 cache),這是鄰近處理器的一種高速存儲器。處理器從 L2 緩存中訪問經(jīng)常修改的數(shù)據(jù)。當兩個處理器共享一個數(shù)據(jù)結(jié)構(gòu),并同時修改那個數(shù)據(jù)結(jié)構(gòu)時,就有可能引發(fā)問題。CPU1 在它的 L2 緩存中包含數(shù)據(jù)的一個副本,而 CPU2 修改了這個共享的數(shù)據(jù)結(jié)構(gòu)。CPU1 L2 緩存中的副本現(xiàn)在是無效的,必須進行更新。CPU1 必須花費大量步驟從主存中檢索數(shù)據(jù),這需要占用額外的處理器周期。圖 1 展示了兩個處理器,它們在各自的 L2 緩存中包含一個共享數(shù)據(jù)結(jié)構(gòu)的一個副本。
圖 1. 共享一個數(shù)據(jù)結(jié)構(gòu)的兩個處理器
在這個例子中,您將查看這個數(shù)據(jù)結(jié)構(gòu)(如清單 4 所示),并分析兩個處理器同時修改這個數(shù)據(jù)結(jié)構(gòu)時出現(xiàn)的情景)。然后觀察數(shù)據(jù)緩存失誤,并考察用來修正這個問題的兩種解決方案。
清單 4. 共享的數(shù)據(jù)結(jié)構(gòu)
struct shared_data_struct { unsigned int data1; unsigned int data1;}
|
清單 5 中的程序使用 clone() 系統(tǒng)調(diào)用和 VM_CLONE 標志生成一個子進程。VM_CLONE 標志會導致子進程和父進程在同一個存儲空間中運行。父線程修改該數(shù)據(jù)結(jié)構(gòu)的第一個元素,而子線程則修改第二個元素。
清單 5. 演示 L2 數(shù)據(jù)緩存失誤的代碼示例
#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 中的命令不帶優(yōu)化地編譯這個示例程序。
清單 6. 用于編譯清單 5 中例子代碼的命令
gcc -o cache-miss cache-miss.c
|
現(xiàn)在您可以用 OProfile 分析上述程序中出現(xiàn)的 L2 數(shù)據(jù)緩存失誤。
對于這個例子,作者在一臺 IBM eServer™ OpenPower™ 710 上執(zhí)行和分析了這個程序,該機器有兩個 POWER5 處理器,并運行 SLES9 Service Pack 1 (SLES9SP1)。將 --list-events 標志傳遞給 opcontrol,以判斷是哪一個事件負責監(jiān)視 L2 數(shù)據(jù)緩存失誤。對于基于 POWER5 處理器的、運行 SLES9SP1 的系統(tǒng),由 PM_LSU_LMQ_LHR_MERGE_GP9 事件監(jiān)視 L2 數(shù)據(jù)緩存失誤。如果您將樣本計數(shù)設置為 1000,比如在這個例子中,那么 OProfile 將從每 1000 個硬件事件抽取一個樣本。如果使用不同的平臺,例如基于 POWER4 處理器的服務器,那么這樣的事件也會有所不同。
使用 清單 7 中的命令分析這個例子代碼,如下所示:
清單 7. 用來分析清單 5 所示例子中的 L2 數(shù)據(jù)緩存失誤的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64# opcontrol --reset# opcontrol --setup –event=PM_LSU_LMQ_LHR_MERGE_GP9:1000# opcontrol --startUsing 2.6+ OProfile kernel interface.Reading module info.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./cache-miss# opcontrol --dump# opcontrol -hStopping 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 1000samples % symbol name47897 58.7470 main33634 41.2530 inc_second
|
在分析來自 opreport 的結(jié)果時,您可以看到,在函數(shù) main() 和 inc_second() 中存在很多緩存失誤。opreport 的 -l 選項將輸出符號信息,而實質(zhì)上輸出的應該只是二進制映像名。同樣,緩存失誤的起因也是兩個處理器修改一個共享的數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)大小為 8 字節(jié),放在一個 128 字節(jié)的緩存行中。
消除數(shù)據(jù)緩存失誤的一種方法是填充數(shù)據(jù)結(jié)構(gòu),使得它的每一個元素都存儲在各自的緩存行中。清單 8 包含一個修改后的結(jié)構(gòu),其中有 124 字節(jié)的填充物。
清單 8. 帶填充物的數(shù)據(jù)結(jié)構(gòu),每個元素放進不同的緩存行中
struct shared_data_struct { unsigned int data1; char pad[124]; unsigned int data1;
|
圖 2 展示了在填充數(shù)據(jù)結(jié)構(gòu)后,如何使得每個處理器上的每個數(shù)據(jù)元素都存儲在各自的緩存行中。
圖 2. 共享填充后的數(shù)據(jù)結(jié)構(gòu)的兩個處理器
像前面那樣重新編譯該程序,但是這一次使用修改后的數(shù)據(jù)結(jié)構(gòu)。然后使用 清單 9 中的命令再次分析結(jié)果。
清單 9. 填充數(shù)據(jù)結(jié)構(gòu)后用于 profile L2 數(shù)據(jù)緩存失誤的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64# opcontrol --reset# opcontrol --setup –event=PM_LSU_LMQ_LHR_MERGE_GP9:1000# opcontrol --startUsing 2.6+ OProfile kernel interface.Reading module info.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./cache-miss# opcontrol --dump# opcontrol -hStopping profiling.Killing daemon.# opreport -l ./cache-miss error: no sample files found: profile specification too strict ?
|
Opreport 表明,由于沒有發(fā)現(xiàn)抽樣數(shù)據(jù),所以可能存在錯誤。然而,隨著對共享數(shù)據(jù)結(jié)構(gòu)的修改,這是可以預期的,因為每個數(shù)據(jù)元素都在自己的緩存行中,所以不存在 L2 緩存失誤。
現(xiàn)在可以考察 L2 緩存失誤在處理器周期上的代價。首先,分析使用未填充的原有共享數(shù)據(jù)結(jié)構(gòu)的代碼(清單 4)。您將進行抽樣的事件是 CYCLES。使用 清單 10 中的命令針對 CYCLES 事件分析這個例子。
清單 10. 用于 profile 清單 5 所示例子中處理器周期數(shù)的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64# opcontrol --reset# opcontrol --setup –event=CYCLES:1000# opcontrol --startUsing 2.6+ OProfile kernel interface.Reading module info.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./cache-miss# opcontrol --dump# opcontrol -hStopping 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 1000samples % symbol name121166 53.3853 inc_second105799 46.6147 main
|
現(xiàn)在,使用 清單 11 中的命令分析使用填充后的數(shù)據(jù)結(jié)構(gòu)的例子代碼(清單 8)。
清單 11. 用于分析使用填充后的數(shù)據(jù)結(jié)構(gòu)的例子中處理器周期數(shù)的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64# opcontrol --reset# opcontrol --setup –event=CYCLES:1000# opcontrol --startUsing 2.6+ OProfile kernel interface.Reading module info.Using log file /var/lib/oprofile/oprofiled.logDaemon started.Profiler running.# ./cache-miss# opcontrol --dump# opcontrol -hStopping 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 1000samples % symbol name104916 58.3872 inc_second74774 41.6128 main
|
不出所料,隨著 L2 緩存失誤數(shù)量的增加,處理器周期數(shù)也有所增加。其主要原因是,與從 L2 緩存取數(shù)據(jù)相比,從主存獲取數(shù)據(jù)代價昂貴。
避免兩個處理器之間緩存失誤的另一種方法是在相同處理器上運行兩個線程。通過使用 Cpu 相似性(affinity),將一個進程綁定到一個特定的處理器,下面的例子演示了這一點。在 Linux 上,sched_setaffinity() 系統(tǒng)調(diào)用在一個處理器上運行兩個線程。 清單 12 提供了原來的示例程序的另一個變體,其中使用 sched_setaffinity() 調(diào)用來執(zhí)行這一操作。
清單 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++; } }}
|
這個例子在同處理器上運行兩個線程,共享數(shù)據(jù)結(jié)構(gòu)存放在一個處理器上的一個 L2 緩存行中。這樣應該可以導致零緩存失誤。使用前面描述的步驟分析緩存失誤,以驗證在一個處理器上運行兩個進程時,是否不存在 L2 緩存失誤。對于數(shù)據(jù)緩存失誤這個問題,第三種解決方法是使用編譯器優(yōu)化,這樣可以減少緩存失誤的數(shù)量。然而,在某些環(huán)境下,這不是一個合適的選擇,您仍然必須分析代碼,并對不良性能做出改正。
結(jié)束語
分析是開發(fā)過程中最困難的任務之一。為了使代碼獲得最佳性能,好的工具是必不可少的。OProfile 就是這樣一種工具,目前它提供了針對 Linux on POWER 的分析功能。對于其他平臺上的可以快速移植到 Linux on POWER 的 Linux,還有其他許多性能和調(diào)試工具。除了處理器事件的類型有所差別外,在基于 POWER 處理器的 Linux 平臺上運行 OProfile 與在其他架構(gòu)上運行 OProfile 是類似的。所以,如果在其他平臺上使用過 OProfile,那么您應該在很短時間內(nèi)就可以知道如何在 Linux on POWER 上運行 OProfile。
致謝
我要感謝 Linda Kinnunen,是她提供了文檔模板并對本文進行了審校,我還要感謝 Maynard Johnson 對本文進行了技術(shù)上的審校。