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

談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的操作,也透過巨集封裝,讓開發者可以便利的使用,這些設計上的思維,都必須要有對編譯器或是平台深度的理解,才能夠達成的.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值