昨天被問到了尾遞迴及編譯器對它的處理相關,一直對它沒有研究過,解釋得很含糊。
回來查了下,記錄如下:
遞迴有線性遞迴(普通的遞迴)和尾遞迴。
由於尾遞迴的特殊性,一般的編譯器會做些特殊處理。因此,在效率和開銷上,比普通遞迴好。
舉個例子,計算n!
1)線性遞迴:
type recurve(long n)
{
return (n == 1) ? 1 : n * recurve(n - 1);
}
2)尾遞迴:
type recurve_tail(long n, long result)
{
return (n == 1) ? result : recurve_tail(n - 1, result * n);
}
再封裝成1)的形式:
type recurve(long n)
{
return (n == 0) ? 1 : recurve_tail(n, 1);
}
分析:
很容易看出, 普通的線性遞迴比尾遞迴更加消耗資源。每次調用都使得調用鏈條不斷加長,系統不得不開闢新的棧進行資料儲存和恢複;而尾遞迴就
不存在這樣的問題, 因為他的狀態完全由n 和 a 儲存,並且,被調用函數返回的值即為要求的值,本函數再沒有作用,於是本函數不再儲存,直接在本函數堆棧上進行遞迴調用,
對於特殊情況,甚至可以不使用記憶體空間,直接在寄存器完成。
編譯器如何判斷是否尾遞迴?
返回的值是函數本身,沒有其它選擇。
看一下上述尾遞迴函式在gcc 4.3.2-1-1下未進行最佳化的編譯結果:
1 .file "rec.c"
2 .text
3 .globl recurve_tail
4 .type recurve_tail, @function
5 recurve_tail:
6 pushl %ebp
7 movl %esp, %ebp
8 subl $24, %esp
9 cmpl $1,
8(%ebp)
10 je .L2
11 movl
12(%ebp), %eax
12 movl %eax, %edx
13 imull 8(%ebp), %edx
14 movl
8(%ebp), %eax
15 subl $1, %eax
16 movl %edx,
4(%esp)
17 movl %eax, (%esp)
18 call
recurve_tail
19 movl %eax, -4(%ebp)
20 jmp .L3
21 .L2:
22 movl
12(%ebp), %eax
23 movl %eax, -4(%ebp)
24 .L3:
25 movl -4(%ebp), %eax
26 leave
27 ret
28 .size recurve_tail, .-recurve_tail
29 .ident "GCC: (Debian 4.3.2-1.1)
4.3.2"
30 .section
.note.GNU-stack,"",@progbits
未進行最佳化,與普通遞迴處理方式相同,新開闢了棧;再看-O3最佳化結果:
1 .file "rec.c"
2 .text
3 .p2align 4,,15
4 .globl recurve_tail
5 .type recurve_tail, @function
6 recurve_tail:
7 pushl %ebp
8 movl %esp, %ebp
9 movl
8(%ebp), %edx
10 movl
12(%ebp), %eax
11 cmpl $1, %edx
12 je .L2
13 .p2align 4,,7
14 .p2align 3
15 .L5:
16 imull %edx, %eax
17 subl $1, %edx
18 cmpl $1, %edx
19 jne .L5
20 .L2:
21 popl %ebp
22 ret
23 .size recurve_tail, .-recurve_tail
24 .ident "GCC: (Debian 4.3.2-1.1)
4.3.2"
25 .section
.note.GNU-stack,"",@progbits
此時,正如上面分析,一直在本空間計算,未開闢新棧。