不同的語言對尾遞歸的支持都有所不同,編譯器的優(yōu)化也不盡相同。我們之前看了C語言的尾遞歸,那么在PHP里又是如何的呢?
PHP對尾遞歸沒有優(yōu)化效果
先來看下實(shí)驗(yàn)。
07 | return factorial( $n -1) * $n ; |
10 | var_dump(factorial(100)); |
如果安裝了XDebug的話,可能會遇到如下錯誤:
1 | Fatal error: Maximum function nesting level of '100' reached, aborting! |
這是XDebug的一個保護(hù)機(jī)制,可以通過max_nesting_level選項(xiàng)來設(shè)置。放開設(shè)置的話,程序還是能夠正常運(yùn)行的。
即便代碼能正常運(yùn)行,只要我們不斷增大參數(shù),程序遲早會報(bào)錯:
1 | Fatal error: Allowed memory size of … bytes exhausted |
為什么呢?簡單點(diǎn)說就是遞歸造成了棧溢出。按照之前的思路,我們可以試下用尾遞歸來消除遞歸對棧的影響,提高程序的效率。
02 | function factorial( $n , $acc ) |
07 | return factorial( $n -1, $acc * $n ); |
11 | var_dump(factorial(100, 1)); |
XDebug同樣報(bào)錯,并且程序的執(zhí)行時間并沒有明顯變化。
1 | Fatal error: Maximum function nesting level of '100' reached, aborting! |
事實(shí)證明,尾遞歸在php中是沒有任何優(yōu)化效果的。
PHP如何消除遞歸
先看看下面的源代碼:
02 | function factorial( $n , $accumulator = 1) { |
07 | return function () use ( $n , $accumulator ) { |
08 | return factorial( $n - 1, $accumulator * $n ); |
12 | function trampoline( $callback , $params ) { |
13 | $result = call_user_func_array( $callback , $params ); |
15 | while ( is_callable ( $result )) { |
22 | var_dump(trampoline( 'factorial' , array (100))); |
現(xiàn)在XDebug不再警報(bào)效率問題了。
注意到trampoline()函數(shù)沒?簡單點(diǎn)說就是利用高階函數(shù)消除遞歸。想更進(jìn)一步了解 call_user_func_array,可以參看這篇文章:PHP函數(shù)補(bǔ)完:call_user_func()與call_user_func_array()
還有很多別的方法可以用來規(guī)避遞歸引起的棧溢出問題,比如說Python中可以通過裝飾器和異常來消滅尾調(diào)用,讓人有一種別有洞天的感覺。
小結(jié)
在C中的尾遞歸優(yōu)化是gcc編譯器做的。一般的線性遞歸修改成為尾遞歸最大的優(yōu)勢在于減少了遞歸調(diào)用棧的開銷。從php那個例子就明顯看出來遞歸開銷對程序的影響。但是并不是所有語言都支持尾遞歸的,即使支持尾遞歸的語言也一般是在編譯階段對尾遞歸進(jìn)行優(yōu)化,比如C語言對尾遞歸的優(yōu)化。在使用尾遞歸對代碼進(jìn)行優(yōu)化的時候,必須先了解這門語言對尾遞歸的支持。
在PHP里,除非能提升代碼可讀性,否則沒有必要使用遞歸;迫不得已之時,最好考慮使用Tail Call或Trampoline等技術(shù)來規(guī)避潛在的棧溢出問題。
|