【PHP】php 遞歸、效率和分析(轉)

遞歸的定義

    遞歸(http:/en.wikipedia.org/wiki/Recursive)是一種函數調用自身(直接或間接)的一種機制,這種強大的思想能夠把某些複雜的概念變得極爲簡單。在計算機科學以外,尤爲是在數學中,遞歸的概念家常便飯。例如:最經常使用於遞歸講解的斐波那契數列即是一個極爲典型的例子,而其餘的例如階層(n!)也能夠轉化爲遞歸的定義(n! = n*(n-1)!.即便是在現實生活中,遞歸的思想也是隨處可見:例如,因爲學業問題你須要校長蓋章,然而校長卻說「只有教導主任蓋章了我纔會蓋章」,當你找到教導主任,教導主任又說:「只有系主任蓋章了我纔會蓋章」...直到你最終找到班主任,在獲得班主任豪爽的蓋章以後,你要依次返回到系主任、教導主任、最後獲得校長的蓋章,過程以下:php

  蓋章的故事雖然索然無味(誰的大學生活沒有點悲催的事情呢?不悲催,怎麼證實咱們年輕過),但卻很好的體現了遞歸的基本思想,也就是遞歸的兩個基本條件:html

  1.   1. 遞歸的退出條件,這是遞歸可以正常執行的必要條件,也是保證遞歸可以正確返回的必要條件。若是缺少這個條件,遞歸就會無限進行下去,直到系統給予的資源耗盡  
  2. (在大多數語言中,都是堆棧空間耗盡),所以,若是你在編程中碰到相似「stack overflow」(C語言中,即棧溢出)和「max nest level of 100 reached」  
  3. (php中,超出遞歸限制)等錯誤,多半是沒有正確的退出條件,致使了遞歸深度過大或者無限遞歸。  
  4.   2. 遞推過程。由一層函數調用進入下一層函數調用的遞推。以n!爲例。在n>1的狀況下。N! = N*(N-1)! 即是該遞歸函數的遞推過程,咱們也能夠簡單的稱爲「遞歸公式」。  

有了這兩個基本條件,咱們便獲得了遞歸的通常模式用代碼能夠描述爲:linux

  1. function Recur(  param ){  
  2.     if(  reach the baseCondition ){  
  3.         Calu();//計算  
  4.         return ;  
  5.     }  
  6.     //else just do it recursively  
  7.     param = modify(param)/修改參數,準備進入下層調用  
  8.     Recur(param);  
  9. }  

有了遞歸的通常模式,咱們即可以輕鬆實現大多的遞歸函數。例如:常常提起的斐波那契數列的遞歸實現,再如,目錄的遞歸訪問:nginx

  1. function ScanDir($path){  
  2.     if(is_dir($path)){  
  3.         $handler = opendir($path);  
  4.         while($dir = readdir($handler)){  
  5.             if($dir == '.' || $dir == '..'){  
  6.                 continue;  
  7.             }  
  8.             if(is_dir($path."/".$dir)){  
  9.                 ScanDir($path."/".$dir."/");  
  10.             }else{  
  11.                 echo "file: ".$path."/".$dir.PHP_EOL;  
  12.             }  
  13.         }  
  14.     }  
  15. }  
  16. ScanDir("./");  

細心的同窗可能發現,咱們在表述的過程當中,屢次使用「層」這個術語。主要有兩大緣由:c++

1. 人們在分析遞歸的過程當中,常用遞歸樹的形式來分析遞歸函數的走向。以斐波那契數列爲例,首先斐波那契數列的定義爲:編程

所以,爲了獲得Fabn)的值,咱們經常須要展開爲「遞歸樹」的形式,以下圖所示:數組

而遞歸的計算過程則是從上而下,從左而右,一旦到達遞歸樹的葉子節點(也就是遞歸的退出條件),便又層層向上返回。以下圖所示(引用網址:http:/www.csharpwin.com/csharpspace/12292r4006.shtml):數據結構


2. 堆棧的結構。函數

跟遞歸有關的另外一個重要的概念是棧,借用百度百科中關於棧的解釋:「Windows,是向低地址擴展數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就肯定的常數),若是申請的空間超過棧的剩餘空間時,將提示overflow。所以,能從棧得到的空間較小。」 在linux系統中,也能夠經過ulimit –s命 令查看系統的最大棧大小。棧的特色是「後進先出」,也就是最後壓入的元素有最高的優先權,每次壓入數據時,棧層層向上疊放,而取數據時,則是從棧頂取出需 要的數據。正是因爲棧的這一特性,使得棧特別適合用於遞歸。具體來講,在遞歸程序運行時,系統會分配額定大小的棧空間,每次函數調用的參數、局部變量、函 數返回地址(稱爲一個棧幀)都會被壓入到棧空間中(稱爲「保護現場」,以便在合適的時候「返回現場」),每次該層的遞歸調用結束後,便無條件(因爲無條 件,使棧溢出攻擊稱爲可能,可參考(http:/wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html )返回到以前保存的返回地址處繼續執行代碼。這樣層層下來,棧的結構恰似一疊有規律的盤子:工具


做爲遞歸的基本實例,如下可用於練習:

 

1. 目錄的遞歸遍歷。

2. 無限分類。

3. 二分查找和合並排序。

4. PHP內置的與遞歸行爲有關的函數(如array_merge_recursive,array_walk_recursive,array_replace_recursive等,考慮它們的實現)

理解遞歸-函數調用的堆棧跟蹤


在c語言中,能夠經過GDB等調試工具跟蹤函數調用的堆棧,從而細緻追蹤函數的運行過程(關於GDB的使用,推薦@左耳朵耗子以前的博客:http:/blog.csdn.net/haoel/article/details/2879 )

而在php中,可使用的調試方法有:

1.原生的print ,echo ,var_dump,print_r等,一般對於較爲簡單的程序,只須要在函數的 關鍵點輸出便可。

2.Php內置的堆棧跟蹤函數:debug_backtrace debug_print_backtrace.

3.xdebug xhprof等調試工具。

爲了方便理解,仍是以斐波那契數列爲例(這裏,咱們假設n必定是非負數):

  1. function fab($n){  
  2.     debug_print_backtrace();  
  3.     if($n == 1 || $n == 0){  
  4.         return $n;  
  5.     }               
  6.     return fab($n - 1) + fab($n - 2);  
  7. }                       
  8. fab(4);   

打印出的斐波那契的調用堆棧是

#0  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(1) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#3  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(0) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#3  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(1) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(3) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(1) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

#0  fab(0) called at [/search/nginx/html/test/Fab.php:8]

#1  fab(2) called at [/search/nginx/html/test/Fab.php:8]

#2  fab(4) called at [/search/nginx/html/test/Fab.php:10]

 

初看這一堆亂七八糟的輸出,彷佛毫無頭緒。其實對於上述的每一行輸出,都包含以下幾項內容:

A. 所在的棧層次,如#0表示是棧頂,#1表示第一層棧幀,#2表示第二層棧幀,依次類推,數字越大,表示所在的棧幀深度越大。

B. 調用的函數和參數。如fab(4)表示實際的執行函數是fab函數,4表示函數的實參。

C. 調用的位置:包括文件名和執行的行數。

實際上,咱們加上一些額外的輸出信息,即可以更加清晰的看到函數的調用堆棧和計算過程,例如:咱們加上函數層次的基本信息:

  1. function fab($n){  
  2.     echo 「-- n = $n ----------------------------」.PHP_EOL;  
  3.     debug_print_backtrace();  
  4.     if($n == 1 || $n == 0){  
  5.         return $n;  
  6.     }               
  7.     return fab($n - 1) + fab($n - 2);  
  8. }                       
  9. fab(4);  

則執行fab(4)以後的調用堆棧爲:

  1. ---- n = 4 ---------------------------------------------  
  2. #0  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  3. ---- n = 3 ---------------------------------------------  
  4. #0  fab(3) called at [/search/nginx/html/test/Fab.php:9]  
  5. #1  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  6. ---- n = 2 ---------------------------------------------  
  7. #0  fab(2) called at [/search/nginx/html/test/Fab.php:9]  
  8. #1  fab(3) called at [/search/nginx/html/test/Fab.php:9]  
  9. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  10. ---- n = 1 ---------------------------------------------  
  11. #0  fab(1) called at [/search/nginx/html/test/Fab.php:9]  
  12. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]  
  13. #2  fab(3) called at [/search/nginx/html/test/Fab.php:9]  
  14. #3  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  15. ---- n = 0 ---------------------------------------------  
  16. #0  fab(0) called at [/search/nginx/html/test/Fab.php:9]  
  17. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]  
  18. #2  fab(3) called at [/search/nginx/html/test/Fab.php:9]  
  19. #3  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  20. ---- n = 1 ---------------------------------------------  
  21. #0  fab(1) called at [/search/nginx/html/test/Fab.php:9]  
  22. #1  fab(3) called at [/search/nginx/html/test/Fab.php:9]  
  23. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  24. ---- n = 2 ---------------------------------------------  
  25. #0  fab(2) called at [/search/nginx/html/test/Fab.php:9]  
  26. #1  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  27. ---- n = 1 ---------------------------------------------  
  28. #0  fab(1) called at [/search/nginx/html/test/Fab.php:9]  
  29. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]  
  30. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]  
  31. ---- n = 0 ---------------------------------------------  
  32. #0  fab(0) called at [/search/nginx/html/test/Fab.php:9]  
  33. #1  fab(2) called at [/search/nginx/html/test/Fab.php:9]  
  34. #2  fab(4) called at [/search/nginx/html/test/Fab.php:11]  

 對該輸出的解釋(注意輸出的前兩列):因爲程序須要計算fab(4)的值。而fab(4)的值依賴於fab(3)fab(2)的值,於是沒法直接計算fab(4)的值,須要將其壓入棧中,對應下圖中的1。fab(4)的左分支爲fab(3),fab(3)的值也沒法直接計算,於是須要將fab(3)也壓入棧中,對應下圖中的2,同理fab(2)也須要壓入棧中,直到遞歸樹的葉子節點。計算完葉子節點後,依次退棧,直到棧爲空,以下圖所示:

 性能表現-遞歸效率分析

 

  昨天在翻閱樸靈的《深刻淺出NODE.js》的時候,看到做者對不一樣的語言作性能 測試時給出的測試結果。大體是:經過簡單的斐波那契數列的遞歸計算,測試不一樣語言的計算時間,從而大體評估不一樣語言的計算性能。其中PHP的計算時間讓我 極爲吃驚:在n=40的狀況下,PHP計算斐波那契數列的耗時爲1m17.728s也就是77.728s,與c語言的0.202s相比,足足差了約380 倍!(測試結果可見下圖)


 

  咱們知道,PHP代碼的執行過程是通過掃描代碼、詞法分析、語法分析等過程,將PHP程序編譯成中間代碼(Opcode字節碼),而後由zend核心引擎負責執行,於是從本質上說,PHP是封裝在C語言基礎上的一個高級語言實現。這樣,因爲PHP編譯過程並無作過多的編譯優化,加之須要在Zend虛擬機上運行,效率與原生C語言相比,必然要大打折扣,可是,竟然會有如此大的差距,仍是不免讓人匪夷所思。

PHP中遞歸的效率爲什麼如此低下(其中一個須要知道的是PHP中不支持尾遞歸優化,這樣會致使樹形遞歸的反覆迭代和重複計算,於是遞歸的效率大大降低,可以容忍的遞歸層次也大大下降。在c/c++中,使用gcc -O2等級以上的編譯時,編譯會對遞歸作相應的優化)?在這篇文章(PHP函數的實現原理及性能分析)中,做者的一個解釋是:「函 數遞歸是經過堆棧來完成的。在php中,也是利用相似的方法來實現。Zend爲每一個php函數分配了一個活動符號表 (active_sym_table),記錄當前函數中全部局部變量的狀態。全部的符號表經過堆棧的形式來維護,每當有函數調用的時候,分配一個新的符號 表併入棧。
當調用結束後當前符號表出棧。由此實現了狀態的保存和遞歸。 對於棧的維護,zend在這裏作了優化。預先分配一個長度爲N的靜態數組來模擬堆棧,這種通 過靜態數組來模擬動態數據結構的手法在咱們本身的程序中也常常有使用,這種方式避免了每次調用帶來的內存分配、銷燬。ZEND只是在函數調用結束時將當前 棧頂的符號表數據clean掉便可。由於靜態數組長度爲N,一旦函數調用層次超過N,程序不會出現棧溢出,這種狀況下zend就會進行符號表的分配、銷 毀,所以會致使性能降低不少。在zend裏面,N目前取值是32。所以,咱們編寫php程序的時候,函數調用層次最好不要超過32。

另外,php bug中也有說明:「PHP 4.0 (Zend) uses the stack for intensive data, rather than using the heap. That means that its tolerance recursive functions is significantly

lower than that of other languages 」

SO, PHP中,若是不是很是必要,咱們建議,最好儘可能少使用遞歸,尤爲是在遞歸層次較大或者沒法估算遞歸的層次時。

因爲時間倉促,文中不免有錯誤,敬請指出,不甚感激。

相關文章
相關標籤/搜索