【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中,若是不是很是必要,咱们建议,最好尽可能少使用递归,尤为是在递归层次较大或者没法估算递归的层次时。

因为时间仓促,文中不免有错误,敬请指出,不甚感激。

相关文章
相关标签/搜索