加入RUN!PC粉絲團
最近新增的精選文章
 
最多人點閱的精選文章
 
 
精選文章 - 開發技術
分享到Plurk
分享到FaceBook
 
Linux Kernel巨集do{...}while(0)的撰寫
文/loda 2011/11/15 下午 06:54:42

不同於過去文章,都是以技術的探索為主,這次的文章,無關乎技術深度,但希望凸顯出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.jpg'>pi_lock);
#ifdef CONFIG_RT_MUTEXES
plist_head_init_raw(&p.jpg'>pi_waiters, &p.jpg'>pi_lock);
p.jpg'>pi_blocked_on = NULL;
#endif
}

(2) 在檔案include/linux/cred.h 中,有如下宣告
#define put_group_info(group_info) \
do { \
if (atomic_dec_and_test(&(group_info).jpg'>usage)) \
groups_free(group_info); \
} while (0)

而在kernel/cred.c中,呼叫 put_group_info 的方式為
......
if (cred.jpg'>group_info)
put_group_info(cred.jpg'>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
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
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
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
-----end-----

funcB
-----box-----
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
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
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時,兩者有如下的差異。


funcA
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

funcB

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》文件中,有關巨集解釋的九個章節,對於理解巨集有很大的幫助,在實際的驗證上可以透過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的操作,也透過巨集封裝,讓開發者可以便利的使用,這些設計上的思維,都必須要有對編譯器或是平台深度的理解,才能夠達成的。

後續,有關這系列文章的規劃,筆者會著重於Android系統與Linux Kernel相關的議題作介紹,希望對於各位能有所助益。


作者簡介:loda
服務於手機產業,熱衷於系統軟體與處理器相關的研發工作,樂於技術分享與實踐.
‧部落格:http://loda.hala01.com/oldarticles/