1. 調試相關的宏
在Linux使用gcc編譯程序的時候,對于調試的語句還具有一些特殊的語法。gcc編譯的過程中,會生成一些宏,可以使用這些宏分別打印當前源文件的信息,主要內容是當前的文件、當前運行的函數和當前的程序行。
具體宏如下:
__FILE__ ?當前程序源文件 (char*)__FUNCTION__ ?當前運行的函數 (char*)__LINE__ ?當前的函數行 (int)
這些宏不是程序代碼定義的,而是有編譯器產生的。這些信息都是在編譯器處理文件的時候動態產生的。
測試示例:
?
#include
int main(void){ ? ?printf("file: %s ", __FILE__); ? ?printf("function: %s ", __FUNCTION__); ? ?printf("line: %d ", __LINE__);
return 0;}
2. # 字符串化操作符
在gcc的編譯系統中,可以使用#將當前的內容轉換成字符串。
程序示例:
#include
#define DPRINT(expr) printf("
int main(void){ ? ?int x = 3; ? ?int y = 5;
DPRINT(x / y); ? ?DPRINT(x + y); ? ?DPRINT(x * y);
return 0;}
執行結果:
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out ?
#expr表示根據宏中的參數(即表達式的內容),生成一個字符串。該過程同樣是有編譯器產生的,編譯器在編譯源文件的時候,如果遇到了類似的宏,會自動根據程序中表達式的內容,生成一個字符串的宏。
這種方式的優點是可以用統一的方法打印表達式的內容,在程序的調試過程中可以方便直觀的看到轉換字符串之后的表達式。具體的表達式的內容是什么,有編譯器自動寫入程序中,這樣使用相同的宏打印所有表達式的字符串。
//打印字符#define debugc(expr) printf("
由于#expr本質上市一個表示字符串的宏,因此在程序中也可以不適用%s打印它的內容,而是可以將其直接與其它的字符串連接。因此,上述宏可以等價以下形式:
//打印字符#define debugc(expr) printf("
總結
#是C語言預處理階段的字符串化操作符,可將宏中的內容轉換成字符串。
3. ## 連接操作符
在gcc的編譯系統中,##是C語言中的連接操作符,可以在編譯的預處理階段實現字符串連接的操作。
程序示例:
#include
#define test(x) test##x
void test1(int a){ ? ?printf("test1 a = %d ", a);}
void test2(char *s){ ? ?printf("test2 s = %s ", s);}
int main(void){ ? ?test(1)(100);
test(2)("hello world");
return 0;}
上述程序中,test(x)宏被定義為test##x, 他表示test字符串和x字符串的連接。
在程序的調試語句中,##常用的方式如下:
#define DEBUG(fmt, args...) printf(fmt, ##args)
替換的方式是將參數的兩個部分以##連接。##表示連接變量代表前面的參數列表。使用這種形式可以將宏的參數傳遞給一個參數。args…是宏的參數,表示可變的參數列表,使用##args將其傳給printf函數。
總結
##是C語言預處理階段的連接操作符,可實現宏參數的連接。
4. 調試宏第一種形式
一種定義的方式:
#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__); ? ?printf(fmt, ##args); ? ? ? ? ? ? ? ? ? ?}
程序示例:
#include
#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?printf("file:%s function: %s line: %d ", __FILE__, __FUNCTION__, __LINE__); ? ?printf(fmt, ##args); ? ? ? ? ? ? ? ? ? ?}
int main(void){ ? ?int a = 100; ? ?int b = 200;
char *s = "hello world"; ? ?DEBUG("a = %d b = %d ", a, b); ? ?DEBUG("a = %x b = %x ", a, b); ? ?DEBUG("s = %s ", s);
return 0;}
總結
上面的DEBUG定義的方式是兩條語句的組合,不可能在產生返回值,因此不能使用它的返回值。
5. 調試宏的第二種定義方式
調試宏的第二種定義方式。
#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ?printf("file:%s function: %s line: %d "fmt, ? ?__FILE__, __FUNCTION__, __LINE__, ##args)
程序示例:
#include
#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ?printf("file:%s function: %s line: %d "fmt, ? ?__FILE__, __FUNCTION__, __LINE__, ##args)
int main(void){ ? ?int a = 100; ? ?int b = 200;
char *s = "hello world"; ? ?DEBUG("a = %d b = %d ", a, b); ? ?DEBUG("a = %x b = %x ", a, b); ? ?DEBUG("s = %s ", s);
return 0;}
總結? ?
fmt必須是一個字符串,不能使用指針,只有這樣才可以實現字符串的功能。
6. 對調試語句進行分級審查
即使定義了調試的宏,在工程足夠大的情況下,也會導致在打開宏開關的時候在終端出現大量的信息。而無法區分哪些是有用的。這個時候就要加入分級檢查機制,可以定義不同的調試級別,這樣就可以對不同重要程序和不同的模塊進行區分,需要調試哪一個模塊就可以打開那一個模塊的調試級別。
一般可以利用配置文件的方式顯示,其實Linux內核也是這么做的,它把調試的等級分成了7個不同重要程度的級別,只有設定某個級別可以顯示,對應的調試信息才會打印到終端上。
可以寫出一下配置文件。
[debug]debug_level=XXX_MODULE
解析配置文件使用標準的字符串操作庫函數就可以獲取XXX_MODULE這個數值。
int show_debug(int level){ ? ?if (level == XXX_MODULE) ? ?{ ? ? ? ?#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ?
? ?printf("file:%s function: %s line: %d "fmt, ? ? ? ?__FILE__, __FUNCTION__, __LINE__, ##args) ? ? ? ? ?} ? ?else if (...) ? ?{ ? ? ? ?.... ? ?}}
7. 條件編譯調試語句
在實際的開發中,一般會維護兩種源程序,一種是帶有調試語句的調試版本程序,另外一種是不帶有調試語句的發布版本程序。然后根據不同的條件編譯選項,編譯出不同的調試版本和發布版本的程序。
在實現過程中,可以使用一個調試宏來控制調試語句的開關。
#ifdef USE_DEBUG ? ? ? ?#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ? ?
?printf("file:%s function: %s line: %d "fmt, ? ? ? ?__FILE__, __FUNCTION__, __LINE__, ##args) ?#else ? ? ? ?#define DEBUG(fmt, args...)
#endif
如果USE_DEBUG被定義,那么有調試信息,否則DEBUG就為空。
如果需要調試信息,就只需要在程序中更改一行就可以了。
#define USE_DEBUG#undef USE_DEBUG
定義條件編譯的方式使用一個帶有值的宏。
#if USE_DEBUG ? ? ? ?#define DEBUG(fmt, args...) ? ? ? ? ? ? ? ?
? ?printf("file:%s function: %s line: %d "fmt, ? ? ? ?__FILE__, __FUNCTION__, __LINE__, ##args) ?#else ? ? ? ?#define DEBUG(fmt, args...)
#endif
可以使用如下方式進行條件編譯。
#ifndef USE_DEBUG#define USE_DEBUG 0#endif
8. 使用do…while的宏定義
使用宏定義可以將一些較為短小的功能封裝,方便使用。宏的形式和函數類似,但是可以節省函數跳轉的開銷。如何將一個語句封裝成一個宏,在程序中常常使用do…while(0)的形式。
#define HELLO(str) do { printf("hello: %s ", str); }while(0)
程序示例:
int cond = 1;if (cond) ? ?HELLO("true");else ? ?HELLO("false");
9. 代碼剖析
對于比較大的程序,可以借助一些工具來首先把需要優化的點清理出來。接下來我們來看看在程序執行過程中獲取數據并進行分析的工具:代碼剖析程序。
測試程序:
#include
#define T 100000
void call_one(){ ? ?int count = T * 1000; ? ?while(count--);}
void call_two(){ ? ?int count = T * 50; ? ?while(count--);}
void call_three(){ ? ?int count = T * 20; ? ?while(count--);}
int main(void){ ? ?int time = 10;
while(time--) ? ?{ ? ? ? ?call_one(); ? ? ? ?call_two(); ? ? ? ?call_three(); ? ?}
return 0;}
編譯的時候加入-pg選項。
deng@itcast:~/tmp$ gcc -pg ?test.c -o test
執行完成后,在當前文件中生成了一個gmon.out文件。
deng@itcast:~/tmp$ ./test ?deng@itcast:~/tmp$ lsgmon.out ?test ?test.cdeng@itcast:~/tmp$?
使用gprof剖析主程序。
deng@itcast:~/tmp$ gprof testFlat profile:
Each sample counts as 0.01 seconds. ?% ? cumulative ? self ? ? ? ? ? ?
?self ? ? total ? ? ? ? ? time ? seconds ? seconds ? ?calls ?ms/call ?ms/call ?name ? ?95.64 ? ? ?
1.61 ? ? 1.61 ? ? ? 10 ? 160.68 ? 160.68 ?call_one ?3.63 ? ? ?1.67 ? ? 0.06 ? ? ? 10 ? ? 6.10 ? ?
6.10 ?call_two ?2.42 ? ? ?1.71 ? ? 0.04 ? ? ? 10 ? ? 4.07 ? ? 4.07 ?call_three
?
其中主要的信息有兩個,一個是每個函數執行的時間占程序總時間的百分比,另外一個就是函數被調用的次數。通過這些信息,可以優化核心程序的實現方式來提高效率。
當然這個剖析程序由于它自身特性有一些限制,比較適用于運行時間比較長的程序,因為統計的時間是基于間隔計數這種機制,所以還需要考慮函數執行的相對時間,如果程序執行時間過短,那得到的信息是沒有任何參考意義的。
將上述程序時間縮短。
#include
#define T 100
void call_one(){ ? ?int count = T * 1000; ? ?while(count--);}
void call_two(){ ? ?int count = T * 50; ? ?while(count--);}
void call_three(){ ? ?int count = T * 20; ? ?while(count--);}
int main(void){ ? ?int time = 10;
while(time--) ? ?{ ? ? ? ?call_one(); ? ? ? ?call_two(); ? ? ? ?call_three(); ? ?}
return 0;}
剖析結果如下。
deng@itcast:~/tmp$ gcc -pg test.c -o testdeng@itcast:~/tmp$ ./test ?deng@itcast:~/tmp$ gprof testFlat profile:
Each sample counts as 0.01 seconds. no time accumulated
% ? cumulative ? self ? ? ? ? ? ? ?self ? ? total ? ? ? ? ? time ? seconds ? seconds ? ?calls ?Ts/call ?Ts/call ?name ? ? ?0.00 ? ? ?0.00 ? ? 0.00 ? ? ? 10 ? ? 0.00 ? ? 0.00 ?call_one ?0.00 ? ? ?0.00 ? ? 0.00 ? ? ? 10 ? ? 0.00 ? ? 0.00 ?call_three ?0.00 ? ? ?0.00 ? ? 0.00 ? ? ? 10 ? ? 0.00 ? ? 0.00 ?call_two
因此該剖析程序對于越復雜、執行時間越長的函數也適用。
那么是不是每個函數執行的絕對時間越長,剖析顯示的時間就真的越長呢?可以再看如下的例子。
#include
#define T 100
void call_one(){ ? ?int count = T * 1000; ? ?while(count--);}
void call_two(){ ? ?int count = T * 100000; ? ?while(count--);}
void call_three(){ ? ?int count = T * 20; ? ?while(count--);}
int main(void){ ? ?int time = 10;
while(time--) ? ?{ ? ? ? ?call_one(); ? ? ? ?call_two(); ? ? ? ?call_three(); ? ?}
return 0;}
剖析結果如下。
deng@itcast:~/tmp$ gcc -pg test.c -o testdeng@itcast:~/tmp$ ./test ?deng@itcast:~/tmp$ gprof testFlat profile:
Each sample counts as 0.01 seconds. ?% ? cumulative ? self ? ? ? ? ? ? ?self ? ? total ? ? ? ? ? time ? seconds ? seconds ? ?calls ?ms/call ?ms/call ?name ? ?101.69 ? ? ?0.15 ? ? 0.15 ? ? ? 10 ? ?15.25 ? ?15.25 ?call_two ?0.00 ? ? ?0.15 ? ? 0.00 ? ? ? 10 ? ? 0.00 ? ? 0.00 ?call_one??0.00??????0.15?????0.00???????10?????0.00?????0.00??call_three
總結
在使用gprof工具的時候,對于一個函數進行gprof方式的剖析,實質上的時間是指除去庫函數調用和系統調用之外,純碎應用部分開發的實際代碼運行的時間,也就是說time一項描述的時間值不包括庫函數printf、系統調用system等運行的時間。這些實用庫函數的程序雖然運行的時候將比最初的程序實用更多的時間,但是對于剖析函數來說并沒有影響。
編輯:黃飛
評論
查看更多