1. 程式人生 > >Android筆記-Linux Kernel Ftrace (Function Trace)解析(非常強悍的效能分析方法)

Android筆記-Linux Kernel Ftrace (Function Trace)解析(非常強悍的效能分析方法)

Android筆記-Linux Kernel Ftrace (Function Trace)解析

[email protected]

by loda

在軟體開發時,通常都會面臨到系統效能調教的需求,我們希望知道哪些區塊的程式碼或函式被執行的次數頻繁,或是佔據較高的處理器時間,以便藉此優化程式碼撰寫的行為,或是改善耗CPU時間的演算法,以Linux平臺來說,OProfile(http://oprofile.sourceforge.net )會是一個大家常推薦的工具,OProfile支援Time-based透過系統Timer中斷蒐集當下執行環境資訊,並加以統計,或基於Event-based,以ARM 來說就是Performance Monitor Unit(CP15)硬體支援的Performance控制單元 (更多資訊可以參考:http://infocenter.arm.com/help/topic/com.arm.doc.dai0195b/index.html),ARM PMU提供例如Instruction/Data Cache使用狀況(miss,write-back,write-buffer..etc),Memory/MMU 存取的狀況, IRQ/FIQ Latency,Branch預測統計,I/D-TCM Status..etc,基於這機制的Profiling可以在影響系統效能最少的情況下,進行System-wide的效能統計資訊. 另一種選擇,則是透過ARM接上JTAG介面,藉由ICE軟體的Profiling功能進行分析.

然而,如果我們希望更明確的知道每個Function被執行的次數 (OProfile Time-based的統計時間夠長就可得出對應的比例,做為決定的依據),執行的流程,與個別時間成本,或是系統在排程 (Scheduling and Wake-up),中斷,Block/Net,或Kernel記憶體配置..等,與Linux Kernel核心物件有關資訊的話,其實Ftrace會是另一個可以輔助的資訊來源,不同於OProfile,Ftrace會透過gcc -pg把每個函式前面都插入呼叫mcount函式的動作,在Branch統計部分,也會把if或是透過likely/unlikely巨集,進行植入是的統計,因此,Ftrace相比OProfile雖然可以提供比較完整的Kernel層級統計資訊,但因為OProfile主要是透過ARM或其他處理器平臺的Performance Monitor單元,因此,OProfile可以在影響系統效能較低的情況下進行統計(ㄟ...Time-based Function profiling也是會影響到被測端的效能的.),但總體而言,都比mcount植入每個函式中,對系統效能的影響更算是輕量. 如何決定應用哪個模組進行效能分析,還是要依據當下開發時的目的與所遇到的問題來做決定.

Ftrace最應該參考的檔案就是Linux Kernel原始碼中位於Documentation/ftrace.txt的檔案,參考該檔案資訊與Google一下,Ftrace作者為在RedHat服務的 Steven Rostedt,主要目的是為Linux Kernel提供一個系統效能分析的工具,以便用以除錯或是改善/優化系統效能,Ftrace為一個以Function Trace為基礎的工具,並包含了包括行程Context-Switch,Wake-Up/Ready到執行的時間成本,中斷關閉的時間,以及是哪些函式呼叫所觸發的,這都有助於在複雜的系統執行下,提供必要資訊以便定位問題.

接下來,我們將介紹GCC對於Ftrace Profiling上,在編譯器層級的支援,以及有關的builtin函式,讓各位清楚這些機制底層運作的原理,最後,並以Ftrace為主,說明個機制的內容,但本文並不會深入探究到各Ftrace模組機制的實作部分,主要只以筆者自己認為值得說明的區塊,來加以說明,對各項細節有興趣的開發者,建議可以自行探究.

GCC “-pg” Profiling 機制與builtin函式對Ftrace Branch Profiling的支援

Ftrace支援在有加入 “likely/unlikely” 條件判斷式位置的Brnch Profiling與對整個核心 if 條件判斷式的Brnch Profiling (當然,選擇後者對效能影響也比較明顯...要做記錄的地方變多了.).使用者可以透過Kernel hacking --->Tracers --->Branch Profiling ---> 來選擇“No branch profiling”,”Trace likely/unlikely profiler” 或 “Profile all if conditionalss”. 對系統進行Branch Profiling的動作. (Ftrace在 config中有這四個設定跟Branch Profiling有關CONFIG_TRACE_BRANCH_PROFILING,CONFIG_BRANCH_PROFILE_NONE,CONFIG_PROFILE_ANNOTATED_BRANCHES與 CONFIG_PROFILE_ALL_BRANCHES)

參考include/linux/compiler.h中的實作,如果選擇“Profile all if conditionalss”,就會把全部的if條件判斷字元,透過gcc precompile定義為巨集 __trace_if,如下所示

#define if(cond, ...) __trace_if( (cond , ## __VA_ARGS__) )

#define __trace_if(cond) /

if (__builtin_constant_p((cond)) ? !!(cond) : /

({ /

int ______r; /

static struct ftrace_branch_data /

__attribute__((__aligned__(4))) /

__attribute__((section("_ftrace_branch"))) /

______f = { /

.func = __func__, /

.file = __FILE__, /

.line = __LINE__, /

}; /

______r = !!(cond); /

______f.miss_hit[______r]++; /

______r; /

}))

如果if 條件式為常數(也就是說編譯器可以在編譯階段就決定好if/else路徑了),就不納入統計,反之,就會根據條件式的結果(______r =0 or 1)統計命中的次數,作為if/else條件設計的參考. (其實,透過likely/unlikely優化編譯階段的Branch Predition是很有幫助的).

如果是設定為”Trace likely/unlikely profiler”,就會把 likely與unlikely巨集定義如下所示

/*

* Using __builtin_constant_p(x) to ignore cases where the return

* value is always the same. This idea is taken from a similar patch

* written by Daniel Walker.

*/

# ifndef likely

# define likely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 1))

# endif

# ifndef unlikely

# define unlikely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 0))

# endif

其中__branch_check__定義如下

#define likely_notrace(x) __builtin_expect(!!(x), 1)

#define unlikely_notrace(x) __builtin_expect(!!(x), 0)

#define __branch_check__(x, expect) ({ /

int ______r; /

static struct ftrace_branch_data /

__attribute__((__aligned__(4))) /

__attribute__((section("_ftrace_annotated_branch"))) /

______f = { /

.func = __func__, /

.file = __FILE__, /

.line = __LINE__, /

}; /

______r = likely_notrace(x); /

ftrace_likely_update(&______f, ______r, expect); /

______r; /

})

函式ftrace_likely_update (位置在kernel/trace/trace_branch.c)實作如下所示,

void ftrace_likely_update(struct ftrace_branch_data *f, int val, int expect)

{

/*

* I would love to have a trace point here instead, but the

* trace point code is so inundated with unlikely and likely

* conditions that the recursive nightmare that exists is too

* much to try to get working. At least for now.

*/

trace_likely_condition(f, val, expect);

/* FIXME: Make this atomic! */

if (val == expect)

f->correct++;

else

f->incorrect++;

}

有關函式 trace_likely_condition的行為在此就不追蹤,只談函式ftrace_likely_update,這函式會統計開發者使用likely/unlikely定義好的if/else區塊順序,跟實際執行時,if/else執行的結果,透過 correct/incorrect累加,我們可以根據Run-Time實際統計的結果,看是否原本likely/unlikely有需要修正的空間(往統計正確的方向去,就可避免處理器Pipeline Flush的機會),以便得到更好的執行效能.

若沒有啟動任何Branch Profiling的動作,則likely與unlikely就只會透過Builtin函式_builtin_expect (在GCC 2.96版本之後支援)進行編譯階段的Branch Predition優化動作,如下宣告

# define likely(x) __builtin_expect(!!(x), 1)

# define unlikely(x) __builtin_expect(!!(x), 0)

編譯器支援的Builtin函式__builtin_constant_p,主要的功能為判斷所給予的值是否為常數(__builtin_constant_p會返回1,若不是常數__builtin_constant_p會返回0),若是則可在編譯時期安排好對應的執行動作,就不需要把全部的行為,都放到編譯後的程式碼,透過執行時期才決定.

以如下程式碼來驗證行為,

#define Y 6

void Func(int Z)

{

int vResult;

vResult=__builtin_constant_p(Z)?(Z*100):-1;

printf("5:Result:%ld/n",vResult);

vResult=(Z*100);

printf("6:Result:%ld/n",vResult);

}

int main()

{

int X=5;

int vResult;

vResult=__builtin_constant_p(X)?(X*100):-1;

printf("1:Result:%ld/n",vResult);

vResult=(X*100);

printf("2:Result:%ld/n",vResult);

vResult=__builtin_constant_p(Y)?(Y*100):-1;

printf("3:Result:%ld/n",vResult);

vResult=(Y*100);

printf("4:Result:%ld/n",vResult);

Func(7);

return;

}

以gcc版本4.1.2來驗證,若以-O0編譯,X為一個區域變數,__builtin_constant_p(X)會返回0,反之,Y為一個定義的常數,__builtin_constant_p(Y)會返回1,而如果把一個常數透過函式引數Z傳遞,因為這個值會被放到堆疊(根據每個處理器的Calling Convention,在ARM上會先放到暫存器R0-R3),導致__builtin_constant_p(Z)返回0,若是以-O1或-O2編譯,則編譯器可以判斷區域變數X的值,__builtin_constant_p(X)會返回1,若是函式函式傳遞的引數Z,_builtin_constant_p(Z)還是會傳回0. 優化的區塊還是以Function本身為單位,並且有開啟優化選項,可以讓Builtin函式發揮更好的效能.

參考GCC檔案,__builtin_constant_p也可以作為Constant變數初始值的指定(檔案建議要在GCC 3.0.1版本之後),如下所示,若 EXPRESSION為常數,則table初始值為該常數,反之則初始值為0.

static const int table[] = {

__builtin_constant_p (EXPRESSION) ? (EXPRESSION) : -1,

};

另一個需要介紹的Builtin函式為 __builtin_expect,這函式的功能主要在提供編譯器Branch Prediction的能力,如以下的程式碼

void FuncA(int X)

{

if(__builtin_expect(X,1))

{

printf("FuncA 1:%ld/n",X*0x100);

}

else

{

printf("FuncA 2:%ld/n",X);

}

}

void FuncB(int X)

{

if(__builtin_expect(X,0))

{

printf("FuncB 1:%ld/n",X*0x100);

}

else

{

printf("FuncB 2:%ld/n",X);

}

}

int main()

{

FuncA(7);

FuncB(8);

return;

}

執行結果為

FuncA 1:700h

FuncB 1:800h

以gcc 4.1.2搭配-O2進行編譯(在這驗證環境下,使用-O0,函式__builtin_expect會沒有效果),執行結果一致,透過反組譯程式碼結果如下

FuncA/B (-O0)

FuncA (-O2) - if(__builtin_expect(X,1))

FuncB(-O2)-if(__builtin_expect(X,0))

push %ebp

mov %esp,%ebp

sub $0x8,%esp

mov 0x8(%ebp),%eax

test %eax,%eax

je 80483a9 <FuncA+0x25>

mov 0x8(%ebp),%eax

shl $0x8,%eax

mov %eax,0x4(%esp)

movl $0x8048500,(%esp)

call 8048298 <[email protected]>

jmp 80483bc <FuncA+0x38>

80483a9:

mov 0x8(%ebp),%eax

mov %eax,0x4(%esp)

movl $0x804850d,(%esp)

call 8048298 <[email protected]>

leave

ret

push %ebp

mov %esp,%ebp

sub $0x8,%esp

mov 0x8(%ebp),%eax

test %eax,%eax

je 80483f2 <FuncA+0x22>

shl $0x8,%eax

mov %eax,0x4(%esp)

movl $0x804853a,(%esp)

call 8048298 <[email protected]>

leave

ret

80483f2 :

movl $0x0,0x4(%esp)

movl $0x8048547,(%esp)

call 8048298 <[email protected]>

leave

ret

push %ebp

mov %esp,%ebp

sub $0x8,%esp

mov 0x8(%ebp),%eax

test %eax,%eax

jne 80483b3 <FuncB+0x23>

movl $0x0,0x4(%esp)

movl $0x804852d,(%esp)

call 8048298 <[email protected]>

leave

ret

80483b3 :

shl $0x8,%eax

mov %eax,0x4(%esp)

movl $0x8048520,(%esp)

call 8048298 <[email protected]>

leave

ret

我們可以看到__builtin_expect(X,1)會優先把if的執行區塊放到連續的程式碼中,而__builtin_expect(X,0)則是會把else的執行區塊,放到前面連續的程式碼中,至於執行時期會執行到if或else的區塊,就根據X條件是否為0來決定.參考GCC檔案,我們也可以搭配條件判斷的寫法

可以參考Linux Kernel中include/linux/compiler.h中的實作,如下所示

# define likely(x) __builtin_expect(!!(x), 1)

# define unlikely(x) __builtin_expect(!!(x), 0)

如果開發者判斷,if的區塊是較常被執行到的,那就應該用likely,例如

if (likely(success))

{….}

else //else區塊可能為error處理邏輯,多數的情況應該希望走的是if的邏輯

{….}

如果開發者判斷else區塊,是希望較常被執行到的,就可以使用unlikely,例如

if (unlikely(error))

{….}

else //success

{….}

處理器本身也有Branch Predition的能力,如果不在有被處理器記憶到的BP Entry中,處理器通常會循序Fetch指令進來,一旦發現分支預測錯誤,就會Flush Pipeline,透過函式__builtin_expect,我們可以在編譯時期,依據傳入值的結果,決定 if/else編譯為機械碼時程式碼排列的順序,減少處理器Pipeline被Flush的機率,這對執行效能也是有很大的幫助.

GCC support for the GNU profiler gprof

有關GNU gprof的介紹, 可以參考網頁http://www.cs.utah.edu/dept/old/texinfo/as/gprof.html .基於GCC對Profiling的支援,開發端可以透過 -pg 的編譯引數,讓gcc 把profiling的功能加入到程式碼中,

以如下程式碼為例,

int FuncA()

{

int i;

int vRet=0;

for(i=0;i<20000;i++)

{

vRet+=i;

}

return vRet;

}

int FuncB(int I)

{

int i;

int vRet=0;

for(i=0;i<9999;i++)

{

vRet+=I+FuncA();

}

return vRet;

}

int main()

{

int vResult;

vResult=FuncA();

vResult=FuncB(vResult);

printf("Result:%ld/n",vResult);

return 0;

}

透過gcc編譯後,有加上 -pg 與沒加上的差異如下

函式名稱

無-pg

有加上-pg

main

lea 0x4(%esp),%ecx

and $0xfffffff0,%esp

pushl 0xfffffffc(%ecx)

push %ebp

mov %esp,%ebp

push %ecx

sub $0x24,%esp

call 8048384 <FuncA>

mov %eax,0xfffffff8(%ebp)

mov 0xfffffff8(%ebp),%eax

mov %eax,(%esp)

call 80483b2 <FuncB>

mov %eax,0xfffffff8(%ebp)

mov 0xfffffff8(%ebp),%eax

mov %eax,0x4(%esp)

movl $0x8048500,(%esp)

call 8048298 <[email protected]>

mov $0x0,%eax

add $0x24,%esp

pop %ecx

pop %ebp

lea 0xfffffffc(%ecx),%esp

ret

lea 0x4(%esp),%ecx

and $0xfffffff0,%esp

pushl 0xfffffffc(%ecx)

push %ebp

mov %esp,%ebp

push %ecx

sub $0x24,%esp

call 804837c <[email protected]> =>Glibc提供的mcount函式

call 80484b4 <FuncA>

mov %eax,0xfffffff8(%ebp)

mov 0xfffffff8(%ebp),%eax

mov %eax,(%esp)

call 80484e7 <FuncB>

mov %eax,0xfffffff8(%ebp)

mov 0xfffffff8(%ebp),%eax

mov %eax,0x4(%esp)

movl $0x8048680,(%esp)

call 804835c <[email protected]>

mov $0x0,%eax

add $0x24,%esp

pop %ecx

pop %ebp

lea 0xfffffffc(%ecx),%esp

ret

FuncA

push %ebp

mov %esp,%ebp

sub $0x10,%esp

movl $0x0,0xfffffffc(%ebp)

movl $0x0,0xfffffff8(%ebp)

jmp 80483a4 <FuncA+0x20>

mov 0xfffffff8(%ebp),%eax

add %eax,0xfffffffc(%ebp)

addl $0x1,0xfffffff8(%ebp)

cmpl $0x4e1f,0xfffffff8(%ebp)

jle 804839a <FuncA+0x16>

mov 0xfffffffc(%ebp),%eax

leave

ret

push %ebp

mov %esp,%ebp

sub $0x10,%e