尾調用
在計算機科學里,尾調用是指一個函數里的最后一個動作是一個函數調用的情形:即這個調用的返回值直接被當前函數返回的情形。這種情形下稱該調用位置為尾位置。若這個函數在尾位置調用本身(或是一個尾調用本身的其他函數等等),則稱這種情況為尾遞歸,是遞歸的一種特殊情形。尾調用不一定是遞歸調用,但是尾遞歸特別有用,也比較容易實現。
在程序運行時,計算機會為應用程序分配一定的內存空間;應用程序則會自行分配所獲得的內存空間,其中一部分被用于記錄程序中正在調用的各個函數的運行情況,這就是函數的調用棧。常規的函數調用總是會在調用棧最上層添加一個新的堆棧幀(stack frame,也翻譯為“棧幀”或簡稱為“幀”),這個過程被稱作“入棧”或“壓棧”(意即把新的幀壓在棧頂)。當函數的調用層數非常多時,調用棧會消耗不少內存,甚至會撐爆內存空間(棧溢出)[1],造成程序嚴重卡頓或意外崩潰。尾調用的調用棧則特別易于優化,從而可減少內存空間的使用,也能提高運行速度。[1]其中,對尾遞歸情形的優化效果最為明顯,尤其是遞歸算法非常復雜的情形。[1]
一般來說,尾調用消除是可選的,可以用,也可以不用。然而,在函數編程語言中,語言標準通常會要求編譯器或運行平臺實現尾調用消除。這讓程序員可以用遞歸取代循環而不喪失性能。
定義
尾調用 (tail call) 指的是一個函數的最后一條語句也是一個返回調用函數的語句。在函數體末尾被返回的可以是對另一個函數的調用,也可以是對自身調用(即自身遞歸調用)
尾遞歸
若函數在尾位置調用自身(或是一個尾調用本身的其他函數等等),則稱這種情況為尾遞歸。尾遞歸也是遞歸的一種特殊情形。尾遞歸是一種特殊的尾調用,即在尾部直接調用自身的遞歸函數。對尾遞歸的優化也是關注尾調用的主要原因。尾調用不一定是遞歸調用,但是尾遞歸特別有用,也比較容易實現。
尾遞歸在普通尾調用的基礎上,多出了2個特征:
- 在尾部調用的是函數自身 (Self-called);
- 可通過優化,使得計算僅占用常量棧空間 (Stack Space)。
優化尾遞歸的分析與示例
對函數調用在尾位置的遞歸或互相遞歸的函數,由于函數自身調用次數很多,遞歸層級很深,尾遞歸優化則使原本 O(n) 的調用棧空間只需要 O(1)。因此一些編程語言的標準要求語言實現進行尾調用消除
以Swift
為例
func sum(_ n: UInt) -> UInt {
if n == 0 {
return 0
}
return n + sum(n - 1)
}
調用sum(5)為例。相應的棧空間變化
sum(5)
5 + sum(4)
5 + (4 + sum(3))
5 + (4 + (3 + sum(2)))
5 + (4 + (3 + (2 + sum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15
可觀察,堆棧從左到右,增加到一個峰值后再計算從右到左縮小,這往往是我們不希望的,所以在C語言等語言中設計for, while, goto
等特殊結構語句,使用迭代、尾遞歸,對普通遞歸進行優化,減少可能對內存的極端消耗。修改以上代碼,可以成為尾遞歸:
func tailSum(_ n: UInt) -> UInt {
func sumInternal(_ n: UInt, current: UInt) -> UInt {
if n == 0 {
return current
} else {
return sumInternal(n - 1, current: current + n)
}
}
return sumInternal(n, current: 0)
}
對比后者尾遞歸對內存的消耗
tailSum(5, 0)
tailSum(4, 5)
tailSum(3, 9)
tailSum(2, 12)
tailSum(1, 14)
tailSum(0, 15)
15
則是線性的。
調用棧
是計算機科學中存儲有關正在運行的子程序的消息的棧。有時僅稱“棧”,但棧中不一定僅存儲子程序消息。幾乎所有計算機程序都依賴于調用棧,然而高級語言一般將調用棧的細節隱藏至后臺。
調用棧最經常被用于存放子程序的返回地址。在調用任何子程序時,主程序都必須暫存子程序運行完畢后應該返回到的地址。因此,如果被調用的子程序還要調用其他的子程序,其自身的返回地址就必須存入調用棧,在其自身運行完畢后再行取回。在遞歸程序中,每一層次遞歸都必須在調用棧上增加一條地址,因此如果程序出現無限遞歸(或僅僅是過多的遞歸層次),調用棧就會產生棧溢出。
在Swift
中編譯器在Debug 模式下并不會對尾遞歸進行優化。我們可以在 scheme 設置中將 Run 的配置 改為 Release。
階乘的例子
//普通遞歸
func notailFactorial(_ n:Int) -> Int {
if n == 1 {
return 1
}
return n * notailFactorial(n-1)
}
//尾遞歸
func factorial(_ n: Int) -> Int {
func iter(product:Int , counter: Int = 1) -> Int {
if product <= 1{
return counter
} else {
return iter(product: product-1, counter: counter+product)
}
}
return iter(product: n)
}