談Linux Kernel巨集 do{…}while(0) 的撰寫方式
by loda
hlchou@mail2000.com.tw
Android/Linux Source Code Tags
App BizOrz
BizOrz.COM
BizOrz Blog
http://loda.hala01.com/oldarticles/
不同於過去文章,都是以技術的探索為主,這次的文章,無關乎技術深度,但希望凸顯出Linux Kernel實作上的巧思.筆者相信對大家會有所收穫,也因此選擇以此為主題.
在程式設計寫作時,巨集Marco是常見的寫法,相信閱讀本文的開發者,也非常熟悉才是.
也因為是基礎知識,大家都認為對巨集的使用都已經了然於心,但其實簡單的事物背後也是有它的思考.
在Trace Linux Kernel原始碼時,常會看到把巨集用 do {….} while(0)的寫法包裝起來,時間久了,也認為這是一個合理的作法,但原因呢?就真的沒有仔細的去思考過,從編譯器的角度來說,用了do{…..} while(0)的寫法,在不開啟編譯優化參數的前提下,由於多了新的判斷,應該是會比起單純的 {….}產生額外的程式碼,影響到執行效能才是(例如,多了CMP或Branch條件判斷).而且主觀上,以Linux Kernel這樣等級的Open Source計畫,應該是不會設計出一個明知會導致效能降低的寫法才是.但沒有實際去驗證過,總是存在心頭上的一個問號.
既然有了這樣的發想,也就有了本文的誕生,在這篇文章中將會透過實際的例子,比對編譯後的程式碼,來確認Linux Kernel如此撰寫的影響.更進一步的來說,會參考Linux Kernel Coding Style與Writing CPP Marcos文章中的案例,藉此說明巨集使用上考量. 希望能對閱讀本文的讀者,帶來收穫.
Linux Kernel 中的例子
接下來,我們以 Linux Kernel中使用到do {….} while(0)的Source Code作說明,藉此了解目前實作的例子.
(1) 在檔案include/linux/spinlock.h 中,有如下宣告
# define raw_spin_lock_init(lock) \
do { \
static struct lock_class_key __key; \
\
__raw_spin_lock_init((lock), #lock, &__key); \
} while (0)
而在kernel/fork.c中,呼叫 raw_spin_lock_init的方式為
static void rt_mutex_init_task(struct task_struct *p)
{
raw_spin_lock_init(&p->pi_lock);
#ifdef CONFIG_RT_MUTEXES
plist_head_init_raw(&p->pi_waiters, &p->pi_lock);
p->pi_blocked_on = NULL;
#endif
}
(2) 在檔案include/linux/cred.h 中,有如下宣告
#define put_group_info(group_info) \
do { \
if (atomic_dec_and_test(&(group_info)->usage)) \
groups_free(group_info); \
} while (0)
而在kernel/cred.c中,呼叫 put_group_info 的方式為
…..
if (cred->group_info)
put_group_info(cred->group_info);
…..
再來,讓我們用實際的案例來驗證do {….} while(0)與{…..}的寫法,並比對透過編譯器產生的結果與Linux Kernel Coding Style文件,了解Linux Kernel對巨集的設計建議
對編譯器而言,DoWhile0會得到比較好的編譯結果嗎?
在本段驗證前,其實,腦中有個念頭,就是是否GCC對這種DoWhile0寫法有比較好的編譯結果,能讓運作效率更佳,所以Linux Kernel才會選擇這樣的設計方式.也因此我們透過如下的代碼來進行驗證,並且會透過Open Source的ARM GCC 4.4與商用版本的ARM RVCT 4.0 分別帶入優化參數 0,1,2 比對產生的編譯結果.
int funcA(int IN_A,int IN_B)
{ int OUT=0; if(IN_A) { OUT=(IN_A+3)*IN_B; } else { OUT=(IN_A+33)*IN_B; } return OUT; } int funcB(int IN_A,int IN_B) { int OUT=0; if(IN_A) do{ OUT=(IN_A+3)*IN_B; }while(0); else do{ OUT=(IN_A+33)*IN_B; }while(0); return OUT; } int main() { int vRet; vRet=funcA(0,3); printf("0 A:%ld\n",vRet); vRet=funcB(0,3); printf("0 B:%ld\n",vRet); vRet=funcA(2,3); printf("2 A:%ld\n",vRet); vRet=funcB(2,3); printf("2 B:%ld\n",vRet); return 0; } |
透過 ARM GCC 以-O0編譯後,執行結果如下
# ./main
0 A:99
0 B:99
2 A:15
2 B:15
使用arm-eabi-objdump -x -D 進行反組譯
比對 funcA與funcB的結果如下所示
funcA | funcB |
0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
4: e28db000 add fp, sp, #0 ; 0×0 8: e24dd014 sub sp, sp, #20 ; 0×14 c: e50b0010 str r0, [fp, #-16] 10: e50b1014 str r1, [fp, #-20] 14: e3a03000 mov r3, #0 ; 0×0 18: e50b3008 str r3, [fp, #-8] 1c: e51b3010 ldr r3, [fp, #-16] 20: e3530000 cmp r3, #0 ; 0×0 24: 0a000005 beq 40 <funcA+0×40> 28: e51b3010 ldr r3, [fp, #-16] 2c: e2833003 add r3, r3, #3 ; 0×3 30: e51b2014 ldr r2, [fp, #-20] 34: e0030392 mul r3, r2, r3 38: e50b3008 str r3, [fp, #-8] 3c: ea000004 b 54 <funcA+0×54> 40: e51b3010 ldr r3, [fp, #-16] 44: e2833021 add r3, r3, #33 ; 0×21 48: e51b2014 ldr r2, [fp, #-20] 4c: e0030392 mul r3, r2, r3 50: e50b3008 str r3, [fp, #-8] 54: e51b3008 ldr r3, [fp, #-8] 58: e1a00003 mov r0, r3 5c: e28bd000 add sp, fp, #0 ; 0×0 60: e8bd0800 pop {fp} 64: e12fff1e bx lr | 68: e52db004 push {fp} ; (str fp, [sp, #-4]!)
6c: e28db000 add fp, sp, #0 ; 0×0 70: e24dd014 sub sp, sp, #20 ; 0×14 74: e50b0010 str r0, [fp, #-16] 78: e50b1014 str r1, [fp, #-20] 7c: e3a03000 mov r3, #0 ; 0×0 80: e50b3008 str r3, [fp, #-8] 84: e51b3010 ldr r3, [fp, #-16] 88: e3530000 cmp r3, #0 ; 0×0 8c: 0a000005 beq a8 <funcB+0×40> 90: e51b3010 ldr r3, [fp, #-16] 94: e2833003 add r3, r3, #3 ; 0×3 98: e51b2014 ldr r2, [fp, #-20] 9c: e0030392 mul r3, r2, r3 a0: e50b3008 str r3, [fp, #-8] a4: ea000004 b bc <funcB+0×54> a8: e51b3010 ldr r3, [fp, #-16] ac: e2833021 add r3, r3, #33 ; 0×21 b0: e51b2014 ldr r2, [fp, #-20] b4: e0030392 mul r3, r2, r3 b8: e50b3008 str r3, [fp, #-8] bc: e51b3008 ldr r3, [fp, #-8] c0: e1a00003 mov r0, r3 c4: e28bd000 add sp, fp, #0 ; 0×0 c8: e8bd0800 pop {fp} cc: e12fff1e bx lr |
可以發現產生的指令集是一致的,再進一步用arm-eabi-gcc 搭配 -O1,O2的優化來編譯,也可以發現,優化的結果與產生的指令集,兩者都是一致的.
在GCC編譯器後,我們改用ARM RVCT 4.0編譯器對上述程式碼進行編譯動作,經過比對,只有在armcc 用-O0時,兩者有如下的差異
funcA | funcB |
0×00000000: e1a02000 . .. MOV r2,r0
0×00000004: e3a00000 …. MOV r0,#0 0×00000008: e3520000 ..R. CMP r2,#0 0x0000000c: 0a000002 …. BEQ {pc}+0×10 ; 0x1c 0×00000010: e2823003 .0.. ADD r3,r2,#3 0×00000014: e0000193 …. MUL r0,r3,r1 0×00000018: ea000001 …. B {pc}+0xc ; 0×24 0x0000001c: e2823021 !0.. ADD r3,r2,#0×21 0×00000020: e0000193 …. MUL r0,r3,r1 0×00000024: e12fff1e ../. BX lr | 0×00000028: e1a02000 . .. MOV r2,r0
0x0000002c: e3a00000 …. MOV r0,#0 0×00000030: e3520000 ..R. CMP r2,#0 0×00000034: 0a000003 …. BEQ {pc}+0×14 ; 0×48 0×00000038: e1a00000 …. MOV r0,r0 0x0000003c: e2823003 .0.. ADD r3,r2,#3 0×00000040: e0000193 …. MUL r0,r3,r1 0×00000044: ea000003 …. B {pc}+0×14 ; 0×58 0×00000048: e1a00000 …. MOV r0,r0 0x0000004c: e2823021 !0.. ADD r3,r2,#0×21 0×00000050: e0000193 …. MUL r0,r3,r1 0×00000054: e1a00000 …. MOV r0,r0 0×00000058: e12fff1e ../. BX lr |
多了三處可以忽略的 “mov r0,r0“ 動作,但其它的編譯結果都是一致的.
總結來說,除了ARM RVCT 4.0的-O0優化參數外,使用ARM GCC或是ARM RVCT 4.0的編譯環境,對 do {….} while(0)與{…..}的寫法,只要使用到-O1或之後的優化參數,最終產生的編譯結果機械碼兩者是一致的.
所以,筆者原先的揣測看來是多想了…@_@.再來讓我們進一步從程式碼撰寫的角度來分析.
對 if/else 區塊的影響
#define Test_DoWhileZero(IN_A,IN_B) \
do { \ if(IN_A) \ { OUT=(IN_A+3)*IN_B;} \ else \ { OUT=(IN_A+33)*IN_B;} \ } while (0) #define Test_Normal(IN_A,IN_B) \ { \ if(IN_A) \ { OUT=(IN_A+3)*IN_B;} \ else \ { OUT=(IN_A+33)*IN_B;} \ } int funcA(int IN_A,int IN_B) { int OUT=0; if(IN_B) Test_DoWhileZero(IN_A,IN_B); else printf("Error IB_B==NULL\n"); return OUT; } int funcB(int IN_A,int IN_B) { int OUT=0; if(IN_B) Test_Normal(IN_A,IN_B); else printf("Error IB_B==NULL\n"); return OUT; } int main() { int vRet; vRet=funcA(0,3); printf("0 A:%ld\n",vRet); vRet=funcB(0,3); printf("0 B:%ld\n",vRet); vRet=funcA(2,3); printf("2 A:%ld\n",vRet); vRet=funcB(2,3); printf("2 B:%ld\n",vRet); return 0; } |
透過arm-eabi-gcc編譯時,會導致如下的錯誤發生
In function ‘funcB’:
error: ‘else’ without a previous ‘if’
原因在於巨集的宣告,如果是 {…….},在使用巨集Test_Normal時又有加上 ; 結尾,就會導致原本的 if/else區塊變成如下情況
if(..)
{
…
};
else
….
導致 if 條件式在else前就已經結尾.
反之,使用do{…}while(0)寫法的巨集,對應到上述用法時,if/else區塊的展開為
if(..)
do{
…
}while(0);
else
…..
並不會影響到原本 if/else區塊的條件判斷正確性,又可以滿足巨集中需要多行程式碼時,的程式碼撰寫需求.
總結來說,採用DoWhile0的寫法,可以滿足之後要用inline函式取代巨集的需求,而用在if/else這種條件判斷時,巨集展該後的程式碼也能無誤運作.最最最重要的是,從實際編譯器產生的機械碼來說,並不會因為如此撰寫,導致系統運作效率的降低.
Linux Kernel Coding Style文件
我們可以參考Linux Kernel文件”Linux kernel coding style” (檔案路徑Documentation/CodingStyle),了解Linux Kernel對於巨集使用的說明. 這份文件共有18章,是Linux Kernel程式開發者值得參考的程式設計說明,跟本文有關的DoWhile0巨集寫法是在第12章 “Macros, Enums and RTL”,筆者大致說明如下
1,要避免巨集影響到執行流程.
如下所示,在巨集中的DoWhile0,存在return返回值,這會影響到使用這巨集模組的執行流程.
#define FOO(x) \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while(0)
2,避免在巨集宣告中,參考到特定的變數名稱
如下所示,使用FOO巨集時,參考到區域變數index
#define FOO(val) bar(index, val)
在使用巨集FOO的函式中,如果沒有宣告區域變數index,就會導致如下錯誤 “error: ‘index’ undeclared (first use in this function)”.
而若是把index宣告為全域變數,然後使用上述的FOO巨集時,就會在編譯時產生如下的錯誤 “warning: built-in function ‘index’ declared as non-function”,
在巨集的宣告時,儘量要避免額外參考到非巨集帶入的變數,可避免在後續使用上,所造成的問題.
3,巨集所帶的參數不應該當做L-Value.
如下所示,把巨集FOO帶參數直接定義為另一個目標值.
#define FOO(val) val
int func()
{
int x=10;
FOO(x)+=30;
return x;
}
這樣的巨集可以正常運作,但一旦把巨集改為inline函式時
inline int FOO(int val)
{
return val;
}
就會導致如下的錯誤 “error: invalid lvalue in assignment”,
4, 巨集定義的運算式與常數必須有括號前後封裝.以避免因為遺忘了運算優先順序的問題,所導致的錯誤.
如下例子所示,
#define CONSTANTA 2&7
#define CONSTEXPA (400+CONSTANTA)
#define CONSTANTB (2&7)
#define CONSTEXPB (400+CONSTANTB)
在巨集展開後,
CONSTEXPA =(400+2&7)=402&7=2
而
CONSTEXPB=(400+(2&7))=400+2=402
避免透過巨集封裝運算式時,因為括號沒有明確的配置,導致原本設計上,規劃之外的錯誤發生.更多這部份的例子,可以參考下一段的案例.
更進一步
可以參考 ”Writing C/C++ Macros”文件(路徑 http://www.ebyte.it/library/codesnippets/WritingCppMacros.html)中,有關巨集解釋的九個章節,對於理解巨集有很大的幫助,在實際的驗證上可以透過GCC -E的參數,驗證C程式碼在巨集展開後的結果.
若你覺得對巨集已經很清楚,不彷試試回答下面三個值的結果.
定義巨集為
#define SquareOf(x) x*x
變數 int vBase=7;
而以下這三個巨集執行結果,應該是多少呢?
SquareOf(vBase) , SquareOf(vBase+1) 與 SquareOf(vBase+vBase).
透過程式碼驗證
#define SquareOf(x) x*x
int main()
{
int vBase=7;
printf("vBase=%d and define SquareOf(x) = x*x \n",vBase);
printf("SquareOf(vBase)=%d \n",SquareOf(vBase));
printf("SquareOf(vBase+1)=%d \n",SquareOf(vBase+1));
printf("SquareOf(vBase+vBase)=%d \n",SquareOf(vBase+vBase));
return 1;
}
搭配gcc -E,可以看到巨集展開後的內容如下
int main()
{
int vBase=7;
printf("vBase=%d and define SquareOf(x) = x*x \n",vBase);
printf("SquareOf(vBase)=%d \n",vBase*vBase);
printf("SquareOf(vBase+1)=%d \n",vBase+1*vBase+1);
printf("SquareOf(vBase+vBase)=%d \n",vBase+vBase*vBase+vBase);
return 1;
}
SquareOf(vBase)為49,而SquareOf(vBase+1)= vBase+1*vBase+1=7+7+1=15. (不是8*8=64),SquareOf(vBase+vBase)= vBase+vBase*vBase+vBase=7+49+7=63. (不是14*14=196).
另一個可能犯錯的例子是,
定義巨集為
#define SumOf(x,y) (x)+(y)
變數 int vBase1=3, vBase2=5;
以下這兩個巨集執行結果,應該是多少呢?
SumOf(vBase1,vBase2) 與 2*SumOf(vBase1,vBase2).
透過程式碼驗證
#define SumOf(x,y) (x)+(y)
int main()
{
int vBase1=3, vBase2=5;
printf("vBase1=%d,vBase2=%d and define SumOf(x,y)=(x)+(y) \n",vBase1,vBase2);
printf("SumOf(vBase1,vBase2)=%d \n",SumOf(vBase1,vBase2));
printf("2*SumOf(vBase1,vBase2)=%d \n",2*SumOf(vBase1,vBase2));
return 1;
}
搭配gcc -E,可以看到巨集展開後的內容如下
int main()
{
int vBase1=3, vBase2=5;
printf("vBase1=%d,vBase2=%d and define SumOf(x,y)=(x)+(y) \n",vBase1,vBase2);
printf("SumOf(vBase1,vBase2)=%d \n",(vBase1)+(vBase2));
printf("2*SumOf(vBase1,vBase2)=%d \n",2*(vBase1)+(vBase2));
return 1;
}
SumOf(vBase1,vBase2)為8,而2*SumOf(vBase1,vBase2)= 2*(vBase1)+(vBase2)=6+5=11. (不是2*8=16).
要讓結果符合預期,SquareOf與 SumOf巨集需修改為如下內容.
#define SquareOf(x) ((x)*(x))
#define SumOf(x,y) ((x)+(y))
結語
本文從DoWhile0的驗證,到參考有關巨集介紹的文件作探討,我們可以知道像是Linux Kernel這樣受矚目的Open Source計畫,在相關的實作上,也確實有它的思考縝密度.在閱讀Linux Kernel Source Code時,包括在判斷if/else優化動作的likely/unlikely巨集,會透過GCC內建函式__builtin_expect在程式碼編譯時進行條件判斷的優化. 或更進一步藉由GCC內建函式__builtin_constant_p判斷常數,讓__branch_check__巨集可以進行Profiling的動作.而有關平台的部份,像是Memory Barrier的操作,也透過巨集封裝,讓開發者可以便利的使用,這些設計上的思維,都必須要有對編譯器或是平台深度的理解,才能夠達成的.