[PHP] Recurrencia, eficiencia y análisis de PHP

Definición de recursividad

 

    La recursividad (http: /en.wikipedia.org/wiki/Recursive) es un mecanismo para que una función se llame a sí misma (directa o indirectamente) Esta poderosa idea puede hacer que algunos conceptos complejos sean extremadamente simples. Fuera de la informática, especialmente en matemáticas, el concepto de recursividad no es infrecuente. Por ejemplo: la secuencia de Fibonacci, que se usa más comúnmente para explicaciones recursivas, es un ejemplo muy típico. Otros como class (n!) También se pueden transformar en una definición recursiva (n! = N * (n-1)! ) Incluso en la vida real, el pensamiento recursivo se puede ver en todas partes: por ejemplo, debido a problemas académicos se necesita el sello del director, pero el director dice: "Sellaré el sello solo si el decano de enseñanza tiene el sello". el decano, enseña El decano también dijo: "Voy a sellar solo si el decano del departamento lo estampa" ... Hasta que finalmente encuentres al director, después de obtener el sello en negrita del director, tienes que volver a la decano, decano de docencia, y finalmente el rector. Sello, el proceso es el siguiente:

 

  Aunque la historia estampada carece de interés (¿cuya vida universitaria no tiene nada que ver con la tristeza? ¿Cómo podemos demostrar que somos jóvenes si no estamos tristes), pero refleja la idea básica de recursividad, es decir, las dos condiciones básicas de recursividad:

  1. La condición de salida recursiva es una condición necesaria para la ejecución normal de la recursividad y una condición necesaria para garantizar que la recursión pueda volver correctamente. Sin esta condición, la recursividad continuará indefinidamente hasta que se agoten los recursos proporcionados por el sistema (en la mayoría de los idiomas, el espacio de la pila se agota). Por lo tanto, si encuentra un "desbordamiento de pila" (C en el idioma, errores como “ stack overflow ”y“ nivel máximo de nido de 100 alcanzado ”(en PHP, exceder el límite de recursividad) se deben principalmente a condiciones de salida incorrectas, lo que resulta en una profundidad de recursión excesiva o una recursividad infinita. 2. Proceso recursivo. La recursividad de una llamada a una función a la siguiente llamada a una función. Tome n! Como ejemplo. En el caso de n> 1. N! = N * (N-1)! Es el proceso recursivo de la función recursiva, también podemos llamarlo simplemente "fórmula recursiva".

Con estas dos condiciones básicas, obtenemos el patrón general de recursividad, que se puede describir mediante código como:

function Recur (param) {if (alcanza la condición base) {Calu (); // calcula return;} // si no, hazlo recursivamente param = modificar (param) / modifica los parámetros, prepárate para entrar en la capa inferior para llamar a Recur (param);}

Con el patrón general de recursividad, podemos implementar fácilmente la mayoría de las funciones recursivas. Por ejemplo: la realización recursiva de la secuencia de Fibonacci, que a menudo se menciona, y el acceso recursivo del directorio:

function ScanDir ($ ruta) {if (is_dir ($ ruta)) {$ manejador = opendir ($ ruta); while ($ dir = readdir ($ handler)) {if ($ dir == '.' || $ dir == '..') {continuar; } if (es_dir ($ ruta. "/". $ dir)) {ScanDir ($ ruta. "/". $ dir. "/"); } else {echo "archivo:". $ ruta. "/". $ dir.PHP_EOL; }}}} ScanDir ("./");

Los estudiantes cuidadosos pueden encontrar que usamos el término "capa" muchas veces en el proceso de expresión. Hay dos razones principales:

1. En el proceso de analizar la recursividad, la gente suele utilizar la forma de árbol recursivo para analizar la tendencia de las funciones recursivas. Tome la secuencia de Fibonacci como ejemplo. Primero, la secuencia de Fibonacci se define como:

 

Por lo tanto, para obtener el valor de Fab (n), a menudo necesitamos expandirnos a la forma de un "árbol recursivo", como se muestra en la siguiente figura:

 

El proceso de cálculo recursivo es de arriba a abajo, de izquierda a derecha, una vez que llega al nodo hoja del árbol recursivo (es decir, la condición de salida recursiva), regresa capa por capa. Como se muestra en la siguiente figura (URL de referencia: http: /www.csharpwin.com/csharpspace/12292r4006.shtml):

 

2. La estructura de la pila.

Otro concepto importante relacionado con la recursividad es la pila. Tomando prestada la explicación de la pila en la Enciclopedia Baidu: " En Windows, la pila es una estructura de datos extendida a direcciones inferiores , que es un área contigua de memoria. Esta oración significa La dirección en la parte superior de la pila y la capacidad máxima de la pila está predefinida por el sistema. En WINDOWS, el tamaño de la pila es 2M (algunos dicen que es 1M, en resumen, es una constante determinada en tiempo de compilación) . Si el espacio solicitado excede el tamaño de la pila Cuando hay espacio libre, se generará un desbordamiento. Por lo tanto, el espacio que se puede obtener de la pila es menor. "En el sistema Linux, también puede usar el comando ulimit -s para ver el tamaño máximo de pila del sistema. La pila se caracteriza por "último en entrar, primero en salir", es decir, el último elemento que se inserta tiene la prioridad más alta. Cada vez que se insertan datos, la pila se apila una encima de la otra, y cuando se obtienen datos, se toma de la parte superior de la pila .datos. Es esta característica de la pila la que la hace especialmente adecuada para la recursividad. Específicamente, cuando el programa recursivo se está ejecutando, el sistema asignará un espacio de pila de un tamaño nominal, y los parámetros, variables locales y direcciones de retorno de función de cada llamada de función (llamado marco de pila) se insertarán en el espacio de pila ( llamado "Proteger la escena" para "regresar a la escena" cuando sea apropiado), después de cada llamada recursiva de esta capa, será incondicional (debido al incondicional, el ataque de desbordamiento de pila es posible, consulte (http: / wenku.baidu. com / view / 7fb00bc2d5bbfd0a7956737d.html  ) Regrese a la dirección de retorno previamente guardada para continuar ejecutando el código. De esta manera, la estructura de la pila es como una pila de placas regulares:

 

Como ejemplo básico de recursividad, se puede utilizar lo siguiente para la práctica:

 

1. Recorrido recursivo de directorios.

2. Clasificación ilimitada.

3. Búsqueda binaria y ordenación combinada.

4. Funciones integradas de PHP relacionadas con el comportamiento recursivo (como array_merge_recursive, array_walk_recursive, array_replace_recursive, etc., considere su implementación)

 

Comprender el seguimiento de la pila de recursividad de la llamada de función

 

 

En lenguaje C, puede rastrear la pila de llamadas a funciones a través de herramientas de depuración como GDB, para rastrear el proceso de ejecución de la función en detalle (para el uso de GDB, recomiendo el blog de @左耳 game : http: /blog.csdn.net/haoel/ article / details / 2879  ).

En php, los métodos de depuración que se pueden utilizar son:

1. Impresión nativa, echo, var_dump, print_r, etc., generalmente para programas más simples, solo es necesario generar puntos clave en la función.

2. Funciones de seguimiento de pila integradas de Php: debug_backtrace y debug_print_backtrace.

3. Herramientas de depuración como xdebug y xhprof.

Para facilitar la comprensión, tome la secuencia de Fibonacci como ejemplo (aquí, asumimos que n debe ser un número no negativo):

function fab ($ n) {debug_print_backtrace (); if ($ n == 1 || $ n == 0) {return $ n;} return fab ($ n - 1) + fab ($ n - 2);} fabuloso (4);

 

La pila de llamadas de Fibonacci impresa es

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

A primera vista, este lío de resultados parece no tener ni idea. De hecho, para cada línea de salida anterior, contiene los siguientes elementos:

A. El nivel de la pila, como # 0 significa la parte superior de la pila, # 1 significa la primera capa del marco de la pila, # 2 significa la segunda capa del marco de la pila, y así sucesivamente, cuanto mayor sea el número, mayor será la profundidad del marco de la pila.

B. Funciones y parámetros a llamar. Por ejemplo, fab (4) indica que la función de ejecución real es la función fab, y 4 indica el parámetro real de la función.

C. La ubicación de la llamada: incluido el nombre del archivo y el número de líneas ejecutadas.

De hecho, podemos ver la pila de llamadas y el proceso de cálculo de la función más claramente agregando información de salida adicional. Por ejemplo, agregamos la información básica del nivel de función:

 

function fab ($ n) {echo “- n = $ n ----------------------------”. PHP_EOL; debug_print_backtrace () ; si ($ n == 1 || $ n == 0) {return $ n;} devuelve fab ($ n - 1) + fab ($ n - 2);} fab (4) ;

Entonces, la pila de llamadas después de ejecutar fab (4) es:

---- n = 4 ------------------------------------------- - 
# 0 fab (4) llamado en [/search/nginx/html/test/Fab.php:11] 
---- n = 3 ----------------- ---------------------------- 
# 0 fab (3) llamado en [/search/nginx/html/test/Fab.php: 9] 
# 1 fab (4) llamado en [/search/nginx/html/test/Fab.php:11] 
---- n = 2 ----------------- ---------------------------- 
# 0 fab (2) llamado en [/search/nginx/html/test/Fab.php: 9] 
# 1 fab (3) llamado en [/search/nginx/html/test/Fab.php:9] 
# 2 fab (4) llamado en [/search/nginx/html/test/Fab.php:11] 
---- n = 1 ------------------------------------------- - 
# 0 fab (1) llamado en [/search/nginx/html/test/Fab.php:9] 
# 1 fab (2) llamado en [/search/nginx/html/test/Fab.php:9]
# 2 fab (3) llamado en [/search/nginx/html/test/Fab.php:9] 
# 3 fab (4) llamado en [/search/nginx/html/test/Fab.php:11] 
- - n = 0 --------------------------------------------- 
# 0 fab (0) llamado en [/search/nginx/html/test/Fab.php:9] 
# 1 fab (2) llamado en [/search/nginx/html/test/Fab.php:9] 
# 2 fab (3) llamado en [/search/nginx/html/test/Fab.php:9] 
# 3 fab (4) llamado en [/search/nginx/html/test/Fab.php:11] 
---- n = 1 --------------------------------------------- 
# 0 fab (1) llamado en [/search/nginx/html/test/Fab.php:9] 
# 1 fab (3) llamado en [/search/nginx/html/test/Fab.php:9] 
# 2 fab ( 4) llamado en [/search/nginx/html/test/Fab.php:11] 
---- n = 2 ----------------------- ----------------------
# 0 fab (2) llamado en [/search/nginx/html/test/Fab.php:9]  
# 1 fab (4) llamado en [/search/nginx/html/test/Fab.php:11]
---- n = 1 ------------------- -------------------------- 
# 0 fab (1) llamado en [/search/nginx/html/test/Fab.php:9] 
# 1 fab (2) llamado en [/search/nginx/html/test/Fab.php:9] 
# 2 fab (4) llamado en [/search/nginx/html/test/Fab.php:11] 
- - n = 0 --------------------------------------------- 
# 0 fab (0) llamado en [/search/nginx/html/test/Fab.php:9] 
# 1 fab (2) llamado en [/search/nginx/html/test/Fab.php:9] 
# 2 fab (4) llamado en [/search/nginx/html/test/Fab.php:11]

 Explicación de la salida (observe las dos primeras columnas de la salida): Porque el programa necesita calcular el valor de fab (4). El valor de fab (4) depende de los valores de fab (3) y fab (2), por lo que el valor de fab (4) no se puede calcular directamente. Debe insertarse en la pila, correspondiente a 1 en la figura siguiente. La rama izquierda de fab (4) es fab (3) y el valor de fab (3) no se puede calcular directamente. Por lo tanto, fab (3) también debe insertarse en la pila, correspondiente a 2 en la figura siguiente. Lo mismo es cierto para fab (2) También debe insertarse en la pila hasta el nodo hoja del árbol recursivo. Después de calcular los nodos hoja, devuelva la pila a su vez hasta que la pila esté vacía, como se muestra en la siguiente figura:

 Análisis de eficiencia recursivo de rendimiento

 

 

  Ayer, cuando estaba leyendo "Non-Simplicity NODE.js" de Pu Ling, vi los resultados de las pruebas dadas por el autor cuando probó el rendimiento de diferentes idiomas. Aproximadamente: A través de un simple cálculo recursivo de la secuencia de Fibonacci, se prueba el tiempo de cálculo de diferentes idiomas, para evaluar de manera aproximada el rendimiento de cálculo de diferentes idiomas. Entre ellos, el tiempo de cálculo de PHP me sorprendió: en el caso de n = 40, el tiempo consumido por PHP para calcular la secuencia de Fibonacci es 1m17.728s, que es 77.728s, que es mucho peor que 0.202s del lenguaje C ¡Aproximadamente 380 veces! (Los resultados de la prueba se pueden ver en la siguiente figura)

 

  Como sabemos, el proceso de ejecución del código PHP es a través de escaneo de código, análisis léxico, análisis de sintaxis y otros procesos, el programa PHP se compila en código intermedio (código de bytes de Opcode) y luego se ejecuta mediante el motor central Zend, por lo que, en esencia, PHP Es una implementación de lenguaje de alto nivel basada en el lenguaje C. De esta manera, debido a que el proceso de compilación PHP no hizo demasiadas optimizaciones de compilación, y la necesidad de ejecutarse en la máquina virtual Zend, la eficiencia está destinada a reducirse considerablemente en comparación con el lenguaje nativo C. Sin embargo, habrá tal gran brecha, es inevitable. Es increíble.

¿Por qué la eficiencia de la recursividad en PHP es tan baja? (Una cosa que debe saber es que PHP no admite la optimización de la recursividad de cola, lo que conducirá a iteraciones repetidas y cálculos repetidos de recursividad de árbol, por lo que la eficiencia de la recursividad se reduce en gran medida, y el El nivel de recursividad que se puede tolerar también disminuye considerablemente. En c / c ++, cuando se usa gcc -O2 o superior, el compilador optimizará la recursividad en consecuencia)? En este artículo ( principio de implementación y análisis de rendimiento de las funciones PHP ), una de las explicaciones del autor es: " La recursividad de la función se realiza a través de la pila. En php, también se implementa de manera similar. Zend proporciona cada función php Un símbolo activo La tabla (active_sym_table) se asigna para registrar el estado de todas las variables locales en la función actual. Todas las tablas de símbolos se mantienen en forma de una pila. Siempre que hay una llamada de función, se asigna una nueva tabla de símbolos y se agrega a la pila.
Cuando Después de la llamada, la tabla de símbolos actual se extrae de la pila. Esto da cuenta de la preservación y recursión del estado. Para el mantenimiento de la pila, zend se optimiza aquí. Asignar previamente una matriz estática de longitud N para simular la pila. El método de simulación de la estructura de datos dinámicos también se utiliza a menudo en nuestros propios programas. Este método evita la asignación de memoria y la destrucción causada por cada llamada. ZEND simplemente limpia los datos de la tabla de símbolos en la parte superior de la pila actual al final de la llamada de función. Sí. Debido a que la longitud de la matriz estática es N, una vez que el nivel de llamada de función excede N, el programa no desbordará la pila. En este caso, zend asignará y destruirá la tabla de símbolos, lo que resultará en un gran rendimiento En zend, el valor actual de N es 32. Por lo tanto, cuando escribimos programas php, el nivel de llamada a la función no debe exceder 32
".

 

另外 , error de php 中 也 有 说明 : “ PHP 4.0 (Zend) usa la pila para datos intensivos, en lugar de usar el montón. Eso significa que sus funciones recursivas de tolerancia son significativamente

más bajo que el de otros idiomas  "

Entonces, en PHP, si no es muy necesario, sugerimos que es mejor usar la recursividad lo menos posible, especialmente cuando el nivel de recursividad es grande o no se puede estimar.

referencias:

1.   http://www.csharpwin.com/csharpspace/12292r4006.shtml

2.  http: /devzone.zend.com/283/recursion-in-php-tapping-unharnessed-power/

3.  http://blog.csdn.net/heiyeshuwu/article/details/5840025

4.  http: /www.nowamagic.net/librarys/veda/detail/2336 

5.  http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html

6.  http://wenku.baidu.com/view/7fb00bc2d5bbfd0a7956737d.html

Supongo que te gusta

Origin blog.csdn.net/kexin178/article/details/112728323
Recomendado
Clasificación