但是一般會說在C裡面使用迴圈會比遞迴有效率
就讓我們來實驗一下GCC 對遞迴的處理吧!
為什麼說遞迴的成本會比迴圈高呢?
因為遞迴會一直做function call,這有不小的 overhaed
每次call function時, 都會複製參數.重新產生區域變數 疊到Stack上
這得付出一定的成本
一些functional language為了提升遞迴的效率,會幫你把遞迴轉換成迴圈以提升效率
以scheme為例,求N階層的function可以這樣寫:
(define (fac n) (if (<= n 0) 1 (* n fac((- n 1)))))
稍微解說一下,在scheme中 if 的結構是這樣的:
(if X a b)
如果X為 true , 則a, 否則 b
所以上面的程式碼寫成大家看得懂得python形式就是:
def fac(n): if n <= 0: return 1 else: return fac(n-1)
然而scheme中有 "尾端遞迴"最佳化的機制,
只要把遞迴寫成尾端遞迴的形式,scheme就可以幫你把遞迴轉換程成迴圈形式以提高效率
像上面的求階層程式改程尾端遞迴的話就是這樣:
(define (fac n) (define (iter product i) (if (> i n) product (iter (* i product) (+ i 1)))) (iter 1 1))
雖然可讀性稍微差了點,不過可以得到更好的效率
尾端遞迴的優化不在C的標準中,
但我們可以來看看大家最常使用的編譯器GCC(the GNU Compiler Collection)能為我們做什麼
一樣求階層的C程式:
/* fac_1.c */ int fac(int n){ if(n <= 0) return 1; else return n * fac(n-1); }
如果我們把他寫成尾端遞迴的形式:
/* fac_2.c */ int fac(int n, int product){ if(n <= 0) return product; else return fac(n-1, product * n); }
(在呼叫的時候要傳 fac(n, 1))
我們可以打這個指令讓GCC把C編譯成組合語言:
gcc fac_2.c -S -c
產生 fac_2.s 內容如下:
這個比較長就懶得解釋,產生出來的內容就是如我們預期的沒有做最佳化(我還沒下參數).file "fac_2.c" .text .globl fac .type fac, @function fac: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl %edi, -4(%rbp) movl %esi, -8(%rbp) cmpl $0, -4(%rbp) jg .L2 movl -8(%rbp), %eax jmp .L3 .L2: movl -8(%rbp), %eax imull -4(%rbp), %eax movl -4(%rbp), %edx subl $1, %edx movl %eax, %esi movl %edx, %edi call fac .L3: leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size fac, .-fac .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits
老老實實的做條件判斷 然後選擇回傳 1 或是 n * fac(n-1)
也就是遞迴的做法-- 不斷地 call function 直到符合結束條件
接著我們來下最佳化參數 :
gcc fac_2.c -S -c -O2
產生的組合語言程式如下:
.file "fac_2.c" .text .p2align 4,,15 .globl fac .type fac, @function fac: .LFB0: .cfi_startproc testl %edi, %edi movl %esi, %eax jle .L2 .p2align 4,,10 .p2align 3 .L4: imull %edi, %eax subl $1, %edi jne .L4 .L2: rep ret .cfi_endproc .LFE0: .size fac, .-fac .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits
很明顯的可以看出裡面沒有 call fac 這個呼叫自己的命令了
這次我來認真的翻譯組合語言吧,內容可以轉成下面的意思
eax, edi, esi都是暫存器名稱 ,我就寫成 a, d, s 大家可以當成變數看
int fac(int d, int s) {int a; a = s; if(d <= 0) goto L2; L4: a = a * d; d = d -1; if(d != 0) goto L4; L2: return a; }
這是一個迴圈型的程式,很神奇吧!
那如果是前一個沒有寫成尾端遞迴的程式呢?
我們來看一下 :
gcc fac_1.c -S -c -O2
得到的結果是:
.file "fac_2.c" .text .p2align 4,,15 .globl fac .type fac, @function fac: .LFB0: .cfi_startproc testl %edi, %edi movl %esi, %eax jle .L2 .p2align 4,,10 .p2align 3 .L4: imull %edi, %eax subl $1, %edi jne .L4 .L2: rep ret .cfi_endproc .LFE0: .size fac, .-fac .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits
一樣會被轉換成迴圈形式!
GCC實在很有智慧啊~
雖然我們應該還是不能奢望所有的遞迴都能這樣被最佳化,
但卻實不少簡單的遞迴是能夠被成功轉換的 酷!
GCC很慢 Orz
返信削除