不要讓遞迴函式fuck大家的cpu
阿新 • • 發佈:2018-11-23
遞迴演算法是大學計算機課程裡面經常會講到的程式設計方法,因為採用這種方法寫出來的程式碼清晰易懂。
但是,在大多數程式設計規範裡面,會**嚴令禁止**使用遞迴函式,原因下面來詳細說明。
首先,由於邏輯錯誤,由直接或間接遞迴,造成遞迴呼叫無法結束(死遞迴),最後肯定會收到
一個"stack overflow"的宕機資訊。就暫且不論了。
下面要詳細討論的是,簡單的遞迴程式碼是如何fuck計算機執行時系統的。
這裡用計算Fibonacci(斐波納契)數列舉例。
由於在數學公式裡,Fibonacci數列就是一個遞迴定義:
Fibonacci(0)=0
Fibonacci (1)=1
Fibonacci(n)=Fibonacci(n-1)+Fibonacci(n-2) (n>=2)
因而,最容易想到的就是用遞迴方法實現:
func fibonacci(n uint64) uint64 {
x := n
if n > 1 {
x = fibonacci(n-1) + fibonacci(n-2)
}
return x
}
然而,這看似清晰簡單的遞迴函式,對計算機執行時系統而言,卻是毀滅性的災難。 由於每次函式呼叫,編譯器都會幫我們向執行棧push引數和返回地址,以便被呼叫函式可以 正確收到呼叫引數和返回。由於正常的函式呼叫層次一般不會太深,而且由於可能會有多線 程或協程(如:goroutine),為了節約資源,所以每一個執行單元的執行時棧一般不會設計 得無限大。 然而這看似簡單的遞迴呼叫,卻是用可能會無限大的引數N,讓執行時系統實現呼叫層次為 2N(N-1 + N-2)的函式呼叫佇列。這種情況,是任何設計靈活的執行時系統都無法承受 的計算模式。 由於go語言要實現goroutine,設計了可動態增長的執行時棧,但是在N=50的時候,以上 程式碼已經不能在可以忍受的時間內給出計算結果。 其實,所有看似只能用遞迴原語寫出的程式碼,都是可以通過一種可以被稱為stack的資料結構 替代,從而用for迴圈實現相同的邏輯。 以下是兩種用for迴圈實現計算Fibonacci函式的方法:
func fibonacci2(n uint64) uint64 {
f0, f1 := uint64(0), uint64(1)
for i := uint64(2); i <= n; i++ {
f0, f1 = f1, f0+f1
}
return f1
}
func fibonacci3(n uint64) uint64 {
var stack Stack
for j := n - 1; j >= 2; j-- {
stack.push(j)
}
f0, f1 := uint64(0), uint64(1)
for _, ok := stack.pop(); ok; _, ok = stack.pop() {
f0, f1 = f1, f0+f1
}
return f1
}
這種通過普通for迴圈實現遞迴函式的方法,由於不需要增加函式呼叫層次,所以對N的大小
可以沒有任何限制。
實測N=10億,以上函式仍然可以在1ns內給出計算結果(當然已經被uint64截斷)。
以下是N=45的時候,以上三個函式的Benchmark結果:
go test -test.bench=".*"
N = 45
testing: warning: no tests to run
Benchmark_Recursive-4 1 10018573100 ns/op
Benchmark_Loop-4 2000000000 0.00 ns/op
Benchmark_Stack-4 2000000000 0.00 ns/op
PASS
ok github.com/vipally/glab/lab2 10.154s
可以顯見,遞迴函式對於計算機系統來說,該是怎樣的災難。
所以,有經驗的技術經理會在專案組裡嚴令禁止使用遞迴函式是明智的。
因此,不要貪圖一時方便,讓悄悄躲在某個角落的遞迴呼叫fuck大家的CPU。
我將以上測試程式碼放在這裡:
https://github.com/vipally/glab/blob/master/lab2/lab2_test.go
歡迎指教。