程式碼的擴展 - macro 與 inline function 的比較
前言
macro 與 inline function 皆是擴展或是重複利用的方法之一。與一般我們重複利用程式碼的作法 —— function —— 不同之處在於,透過 macro 或是 inline function,程式在運行時可以減少主程式 subroutine 間 push / pop 的步驟,近一步加快執行速度。
兩者間最主要的不同處在於擴展的時機。Macro 會於前處理階段由 preprocessor 進行文字替代,至於 inline function 則在編譯階段由 Compiler 進行擴展。但無論是 macro 或者是 inline function ,皆會因為程式碼的複製導致最終大小,相較使用 function 方式來重複利用程式碼大。 在這篇文章中,我們將會解釋 macro 與 inline function 各自的原理,並透過實際的例子比較兩者之間的差異。
Macro
Macro 是一種透過 preprocessor 將程式碼展開的方式。我們在 #:前處理器的語言 中提過幾個前置處理的指令:File Inclusion、Macro Definition 與 Conditional Compilation。其中,透過 macro definition 的指令就能幫助我們定義一個像函式般的的巨集1。 我們來簡單的複習一下巨集的定義方式。
1#define identifier(parameters, ...) replacement-list
透過上述的程式碼片段,我們將 identfier
定義為一個 macro,所有程式碼中出現的 identifier
都會依序替代成 replacement-list 的內容。一個簡單的巨集如下,我們定義了一個 MAX
的巨集,來比較兩個數的大小,並將大者進行回傳。
1#define MAX(x, y) (x) > (y) ? (x) : (y)
定義 MAX(x, y)
的巨集後,所有程式碼中出現的 MAX(x, y)
皆會在前置處理階段被替換為 (x) > (y) ? (x) : (y)
。
注意事項
常見的錯誤定義方式 a. 誤加等號
=
b. 誤加分號;
以括號保護變數
在上述程式碼中,
TWICE(3 + 5)
會以下面所示的方式展開1M = 2 * 3 + 5
導致
M
的值為11
,而非預期的16
,需要特別的注意。
Inline Function
Inline function 直譯的話中文是內嵌函式,顧名思義就是把函式的內容直接鑲嵌在程式碼中。我們有兩種方式可以使用這類型的函式,加速程式運行的速度。其一,透過 inline
修飾字實現;其二,交由編譯器決定。
對於第一個方法,在函示的前方加入修飾字 inline
,則編譯階段編譯器會將該函式內嵌至程式碼內。以下面的例子來說,max(x, y)
就會在編譯時期展開。
至於第二個方法編譯器的優化中,Compiler 會自動將較短的函式於編譯時期自行展開來提升程式執行的效能。
透過 GCC 觀察 inline 與 macro
macro 的觀察
我們可以透過 -E
的參數來觀察經過 preprossor 處理後的程式。首先,我們先來看原先的程式碼:
經由 gcc -E -o test_macro.i test_macro.c
後,我們得到如是結果:
若我們嘗試在上述注意事項 2. 所提及的項目的話,將程式碼改成以下形式:
1/* test_macro_02.c */
2#define MAX(x, y) x > y? x: y
3
4int main () {
5 int i=4, j=5;
6 MAX(i++, j);
7}
再經由 -E
參數轉換成 .i
檔後,可以得到如下結果。我們會發現,由於 macro 是將文字取代,因此 i
這個變數在判斷 MAX
的過程中,被遞增了兩次。導致最後 i
的值為 6
。
inline
我們可以透過 gcc -S
的參數來觀察編譯完後程式的組合語言。以下兩段程式碼分別為使用 inline 與不使用 inline 的範例。
我們將這兩段程式碼編譯成組合語言,並對兩者進行觀察。第一段為使用 inline
修飾字的程式片段,第二段則為不使用的片段。
使用 inline
關鍵字
1 .section __TEXT,__text,regular,pure_instructions
2 .build_version macos, 12, 0 sdk_version 12, 1
3 .globl _main ## -- Begin function main
4 .p2align 4, 0x90
5_main: ## @main
6 .cfi_startproc
7## %bb.0:
8 pushq %rbp
9 .cfi_def_cfa_offset 16
10 .cfi_offset %rbp, -16
11 movq %rsp, %rbp
12 .cfi_def_cfa_register %rbp
13 subq $16, %rsp
14 movl $0, -4(%rbp)
15 movl $4, -8(%rbp)
16 movl $3, -12(%rbp)
17 movl -8(%rbp), %eax
18 movl %eax, %ecx
19 addl $1, %ecx
20 movl %ecx, -8(%rbp)
21 cmpl -12(%rbp), %eax
22 jle LBB0_2
23## %bb.1:
24 movl -8(%rbp), %eax
25 movl %eax, %ecx
26 addl $1, %ecx
27 movl %ecx, -8(%rbp)
28 movl %eax, -16(%rbp) ## 4-byte Spill
29 jmp LBB0_3
30LBB0_2:
31 movl -12(%rbp), %eax
32 movl %eax, -16(%rbp) ## 4-byte Spill
33LBB0_3:
34 movl -16(%rbp), %esi ## 4-byte Reload
35 leaq L_.str(%rip), %rdi
36 movb $0, %al
37 callq _printf
38 movl -8(%rbp), %esi
39 leaq L_.str(%rip), %rdi
40 movb $0, %al
41 callq _printf
42 movl -4(%rbp), %eax
43 addq $16, %rsp
44 popq %rbp
45 retq
46 .cfi_endproc
47 ## -- End function
48 .section __TEXT,__cstring,cstring_literals
49L_.str: ## @.str
50 .asciz "%d\n"
51
52.subsections_via_symbols
未使用 inline
關鍵字
1 .section __TEXT,__text,regular,pure_instructions
2 .build_version macos, 12, 0 sdk_version 12, 1
3 .globl _twice ## -- Begin function twice
4 .p2align 4, 0x90
5_twice: ## @twice
6 .cfi_startproc
7## %bb.0:
8 pushq %rbp
9 .cfi_def_cfa_offset 16
10 .cfi_offset %rbp, -16
11 movq %rsp, %rbp
12 .cfi_def_cfa_register %rbp
13 movl %edi, -4(%rbp)
14 movl -4(%rbp), %eax
15 shll $1, %eax
16 popq %rbp
17 retq
18 .cfi_endproc
19 ## -- End function
20 .globl _main ## -- Begin function main
21 .p2align 4, 0x90
22_main: ## @main
23 .cfi_startproc
24## %bb.0:
25 pushq %rbp
26 .cfi_def_cfa_offset 16
27 .cfi_offset %rbp, -16
28 movq %rsp, %rbp
29 .cfi_def_cfa_register %rbp
30 movl $3, %edi
31 callq _twice
32 xorl %eax, %eax
33 popq %rbp
34 retq
35 .cfi_endproc
36 ## -- End function
37.subsections_via_symbols
從上面兩個例子我們可以觀察到,使用 inline
修飾字,在編譯階段 Compiler 便將其攤開至 _main
的 subroutine 之中。然而,若未使用 inline
修飾字的一般函式,我們可以看到名為 _twice
的 subroutine,每當我們要使用該函式時,就產生額外的 push / pop 動作,使得程式的執行較 inline function 緩慢。
Macro v.s. Inline Function
以下是兩者與一般函式的比較。三者執行的時機不同,一般 function 是在執行時期以 subroutine 的方式被呼叫,而 macro 與 inline 則分別是在 preprocessing 與 compiling 時期被展開取代。從宣告或是定義的位置來看,macro 會在程式的開頭進行定義,而 inline function 與一般函式相同,在程式的任何位置皆可宣告。對於一般函示或是內嵌函式來說,終止的符號是右大括號,然而 macro 則無特定終止符號,是以換行作為終止。當今天我們為了可讀性要跨行書寫 macro 時,可以透過在該行底端加入 \
符號繼續書寫。
相較於 macro 需要明確定義,有的時候 compiler 會自動將較短小的函式轉換成 inline function。儘管兩者都因為擴展、取代的關係,使得程式的大小增加,但無論是 macro 或是 inline function 都也因此提升了程式的執行速度。 我們可以以下表來一覽兩者之間的差別:
表一:
Function | Macro | Inline | |
---|---|---|---|
執行時期 | Runtime | Preprocessing | Compiling |
使用方式 | type function (arguments) | #define | inline |
宣告位置 | 任何 | 須在程式的開頭宣告 | 可在 class 中,或外面 |
終止時機 | 大括號 } | 無,以 newline 作為終止 | 大括號 } |
作法 | push / pop | text substitue | function substitue |
Debugging | 容易 | 困難 | 容易 |
Automation | No | 須明確定義 | class 中較短的 function 會被 Compiler 自動 inline |
Expansion | No | Always | 可下參數停止 function 展開 |
速度 | 較慢 | 較快 | 較快 |
空間使用 | 較小 | 較多 | 較多 |
小結
在這篇文章中,我們介紹了除了一般函示外,兩種可重複利用程式碼的方式 —— 巨集 macro 與 內嵌函式 inline function。我們各舉了幾個例子來說明兩者的使用方式與使用時應該注意的事項,以及常見的錯誤好比說巨集的定義方式2。之後,我們又透過 GCC3 工具的協助觀察 macro 與 inline function 在不同階段的變化。並透過 表一: 比對兩者與一般函示之間的異同。
若對 preprocessor 有興趣或是尚有疑問的的話,可以參考前面的文章 #:前置處理器的語言.md 。