談Linux Kernel巨集 do{…}while(0) 的撰寫方式

Posted by Lawpig on 6月 03, 2017 | No comments

談Linux Kernel巨集 do{…}while(0) 的撰寫方式

談Linux Kernel巨集 do{…}while(0) 的撰寫方式
by loda
hlchou@mail2000.com.tw
不同於過去文章,都是以技術的探索為主,這次的文章,無關乎技術深度,但希望凸顯出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的結果如下所示
funcAfuncB
0:    e52db004       push     {fp}     ; (str fp, [sp, #-4]!)
4:    e28db000       add      fp, sp, #0          ; 0x0
8:    e24dd014       sub       sp, sp, #20        ; 0x14
c:    e50b0010       str         r0, [fp, #-16]
10:   e50b1014       str         r1, [fp, #-20]
14:   e3a03000       mov     r3, #0   ; 0x0
18:   e50b3008       str         r3, [fp, #-8]
1c:    e51b3010       ldr         r3, [fp, #-16]
20:   e3530000       cmp     r3, #0   ; 0x0
24:   0a000005       beq       40 <funcA+0x40>
28:   e51b3010       ldr         r3, [fp, #-16]
2c:    e2833003       add      r3, r3, #3           ; 0x3
30:   e51b2014       ldr         r2, [fp, #-20]
34:   e0030392       mul      r3, r2, r3
38:   e50b3008       str         r3, [fp, #-8]
3c:    ea000004       b           54 <funcA+0x54>
40:   e51b3010       ldr         r3, [fp, #-16]
44:   e2833021       add      r3, r3, #33        ; 0x21
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          ; 0x0
60:   e8bd0800       pop      {fp}
64:   e12fff1e          bx         lr
68:   e52db004       push     {fp}     ; (str fp, [sp, #-4]!)
6c:    e28db000       add      fp, sp, #0          ; 0x0
70:   e24dd014       sub       sp, sp, #20        ; 0x14
74:   e50b0010       str         r0, [fp, #-16]
78:   e50b1014       str         r1, [fp, #-20]
7c:    e3a03000       mov     r3, #0   ; 0x0
80:   e50b3008       str         r3, [fp, #-8]
84:   e51b3010       ldr         r3, [fp, #-16]
88:   e3530000       cmp     r3, #0   ; 0x0
8c:    0a000005       beq       a8 <funcB+0x40>
90:   e51b3010       ldr         r3, [fp, #-16]
94:   e2833003       add      r3, r3, #3           ; 0x3
98:   e51b2014       ldr         r2, [fp, #-20]
9c:    e0030392       mul      r3, r2, r3
a0:   e50b3008       str         r3, [fp, #-8]
a4:   ea000004       b           bc <funcB+0x54>
a8:   e51b3010       ldr         r3, [fp, #-16]
ac:    e2833021       add      r3, r3, #33        ; 0x21
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          ; 0x0
c8:    e8bd0800       pop      {fp}
cc:    e12fff1e          bx         lr
可以發現產生的指令集是一致的,再進一步用arm-eabi-gcc 搭配 -O1,O2的優化來編譯,也可以發現,優化的結果與產生的指令集,兩者都是一致的.
在GCC編譯器後,我們改用ARM RVCT 4.0編譯器對上述程式碼進行編譯動作,經過比對,只有在armcc 用-O0時,兩者有如下的差異
funcAfuncB
0x00000000:    e1a02000    . ..    MOV      r2,r0
0x00000004:    e3a00000    ….    MOV      r0,#0
0x00000008:    e3520000    ..R.    CMP      r2,#0
0x0000000c:    0a000002    ….    BEQ      {pc}+0x10 ; 0x1c
0x00000010:    e2823003    .0..    ADD      r3,r2,#3
0x00000014:    e0000193    ….    MUL      r0,r3,r1
0x00000018:    ea000001    ….    B        {pc}+0xc ; 0x24
0x0000001c:    e2823021    !0..    ADD      r3,r2,#0x21
0x00000020:    e0000193    ….    MUL      r0,r3,r1
0x00000024:    e12fff1e    ../.    BX       lr
0x00000028:    e1a02000    . ..    MOV      r2,r0
0x0000002c:    e3a00000    ….    MOV      r0,#0
0x00000030:    e3520000    ..R.    CMP      r2,#0
0x00000034:    0a000003    ….    BEQ      {pc}+0x14 ; 0x48
0x00000038:    e1a00000    ….    MOV      r0,r0
0x0000003c:    e2823003    .0..    ADD      r3,r2,#3
0x00000040:    e0000193    ….    MUL      r0,r3,r1
0x00000044:    ea000003    ….    B        {pc}+0x14 ; 0x58
0x00000048:    e1a00000    ….    MOV      r0,r0
0x0000004c:    e2823021    !0..    ADD      r3,r2,#0x21
0x00000050:    e0000193    ….    MUL      r0,r3,r1
0x00000054:    e1a00000    ….    MOV      r0,r0
0x00000058:    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的操作,也透過巨集封裝,讓開發者可以便利的使用,這些設計上的思維,都必須要有對編譯器或是平台深度的理解,才能夠達成的.

0 意見:

張貼留言