Android筆記-Linux Kernel Ftrace (Function Trace)解析(非常強悍的效能分析方法)
Android筆記-Linux Kernel Ftrace (Function Trace)解析
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