Recolección de basura Esta sección explica las ventajas del nuevo mecanismo de Recoleción de basura (GC por sus siglas en inglés de «Garbage Collection») de la versión 5.3 de PHP. Introducción al contador de referencias Una variable en PHP se almacena en un contenedor llamado "zval". Un contenedor zval contiene, además del tipo de la variable y su valor, dos bits adicionales de información. Al primero se le llama "is_ref" y contiene un valor booleano que indica si la variable es parte o no de un "conjunto de referencias". Con este bit, el motor de PHP sabe diferenciar entre variables normales y referencias. Puesto que PHP permite referencias definidas por el usuario, tal y como se crean con el operador &, un contenedor zval tiene también un mecanismo contador de referencias para optimizar el uso de memoria. Esta segunda pieza adicional de información, llamada "refcount", contiene el número de variables (también llamadas símbolos) que apuntan a este contenedor zval. Todos los símbolos se almacenan en una tabla de símbolos, de las cuales hay una por cada ámbito. Hay un ámbito para el script principal (es decir, el solicitado por el navegador), además de uno por cada función o método. Se crea un contenedor zval al crear una nueva variable con un valor constante, como por ejemplo: Creación de un nuevo contenedor zval ]]> En este caso, el nombre del nuevo símbolo, a, se crea en el ámbito actual y se crea un nuevo contenedor de variable con el tipo string y el valor nuevo string. El bit "is_ref" se establece por omisión a &false; dado que no se ha creado ninguna referencia en el espacio del usuario. "refcount" se establece a 1, pues solo hay un símbolo que haga uso de este contenedor de variable. Tenga en cuenta que si "refcount" es 1, "is_ref" siempre valdrá &false;. Si tiene Xdebug instalado, puede mostrar esta información llamando a xdebug_debug_zval. Mostrar información de zval ]]> &example.outputs; Al asignar esta variable a otro nombre de variable, se incrementará refcount. Incremento de refcount de un zval ]]> &example.outputs; Aquí, refcount vale 2, pues el mismo contenedor de variable está vinculado tanto por a como por b. PHP es lo suficiente inteligente para no copiar el contenedor de variable en sí cuando no es necesario. Los contenedores de variables se destruyen cuando "refcount" llega a cero. "refcount" se decrementa en uno cuando alguno de los símbolos vinculados al contenedor de variable abandona su ámbito (p.ej. cuando finaliza una función) o cuando se llama a unset con un símbolo. El siguiente ejemplo muestra esto: Decremento de refcount de zval ]]> &example.outputs; Si ahora llamáramos a unset($a);, el contenedor de variable, incluyendo tanto el tipo como el valor, se eliminarían de la memoría. Tipos compuestos Las cosas se complican con tipos compuestos tales como arrays y object. En lugar de un valor de tipo scalar, los arrays y objects almacenan sus propiedades en su propia tabla de símbolos. Esto significa que el siguiente ejemplo crea tres contenedores zval: Crear un zval de tipo <type>array</type> 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); ?> ]]> &example.outputs.similar; (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 ) ]]> Gráficamente Los zval de un array simple Los tres contenedores zval son: a, meaning, y number. Se aplican reglas similares a la hora de incrementar y decrementar "refcounts". Abajo, añadimos otro elemento al array, y fijamos su valor al contenido de un elemento ya existente: Añadir un elemento existente a un array 'life', 'number' => 42 ); $a['life'] = $a['meaning']; xdebug_debug_zval( 'a' ); ?> ]]> &example.outputs.similar; (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life' ) ]]> Gráficamente Los zval de un array simple con una referencia A partir de la salida de Xdebug, vemos que tanto el antiguo como el nuevo elemento del array apuntan a un contenedor zval cuyo "refcount" vale 2. Pese a que Xdebug muestra dos contenedores zval con valor 'life', son el mismo. La función xdebug_debug_zval no muestra esto, aunque podria comprobarse mostrando también el puntero de memoria. Eliminar un elemento del array es como eliminar un símbolo de un ámbito. Al hacerlo, el "refcount" del contenedor al que apunta el elemento del array se decrementa. De nuevo, cuando "refcount" alcanza cero, el contenedor de la variable se elimina de la memoria. Un ejemplo que muestra esto: Eliminar un elemento de un array 'life', 'number' => 42 ); $a['life'] = $a['meaning']; unset( $a['meaning'], $a['number'] ); xdebug_debug_zval( 'a' ); ?> ]]> &example.outputs.similar; (refcount=1, is_ref=0)='life' ) ]]> Ahora, las cosas se vuelven interesantes si añadimos al propio array como elemento del array, como veremos en el siguiente ejemplo, en el que también usaremos el operador de referencia, ya que de lo contrario PHP crearía una copia: Añadir el propio array como elemento de sí mismo ]]> &example.outputs.similar; (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... ) ]]> Gráficamente Los zval para un array que contiene una referencia circular Puede verse que tanto la variable de tipo array (a) como el segundo elemento (1) apuntan ahora a un contenedor de variable que tiene un "refcount" de 2. Los "..." mostrados arriba indican que hay una referencia cíclica, lo cual, por supuesto, significa que en este caso los "..." apuntan al array original. Al igual que antes, al eliminar una variable se elimina el símbolo y el contador de referencias del contenedor de variable al que apunte se decrementa en uno. De modo que, si eliminamos la variable $a después de ejecutar el código anterior, el contador de referencias del contenedor de variable al que apuntan tanto $a como el elemento "1" se decrementa en uno, de "2" a "1". Se puede representar así: Eliminar <varname>$a</varname> (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... ) ]]> Gráficamente Los zval después de eliminar un array con referencia circular mostrando la fuga de memoria Problemas de limpieza Pese a que ya no hay ningún símbolo en ningún ámbito que apunte a esta estructura, esta no se puede limpiar ya que el elemento "1" del array todavía apunta al mismo array. Al no haber un símbolo externo que apunte a él, no hay forma por la que el usuario pueda eliminar esta estructura; por tanto tenemos una fuga de memoria. Afortunadamente, PHP limpiará esta estructura de datos al finalizar la petición, aunque hasta entonces esté ocupando un valioso espacio de memoria. Esta situación ocurre a menudo si se está implementando un algoritmo de análisis o en otras situaciones en las que un nodo hijo apunte de nuevo a un elemento "padre". Por supuesto, esta situación también puede suceder con objetos, donde es más frecuente que ocurra, ya que los objetos siempre se usan implícitamente por referencia. Esto no debería ser un problema si sólo ocurre una o dos veces, pero si estas fugas de memoria suceden miles, o incluso millones de veces, lógicamente esto comenzaría a ser un problema. Es especialmente problemático en scripts de larga duración, tales como demonios donde básicamente nunca terminan las peticiones, o en un gran conjunto de pruebas unitarias. Esto último causó problemas al ejecutar las pruebas unitarias del componente Template de la biblioteca eZ Componentes. En algunos casos, podrían ser necesarios 2 GB de memoria que quizás no los tenga el servidor de pruebas. Recolección de referencias cíclicas Tradicionalmente, los mecanismos que contabilizan las referencias en memoria, tal como el que empleaba PHP anteriormente, fallaban al abordar las fugas de memoria de referencias cíclicas. Sin embargo, desde PHP 5.3.0 se implementa el algoritmo síncrono del artículo Recolección de ciclos concurrentes en sistemas de contabilidad de referencias que resuelve este asunto. Una explicación detallada del funcionamiento del algoritmo queda más allá del objetivo de esta sección, aunque aquí explicaremos el mecanismo básico. Antes de nada, debemos establecer unas reglas básicas. Si se incrementa un refcount, este aún sigue en uso: no es basura. Si se decrementa el refcount y llega a cero, el zval puede liberarse. Esto significa que la recolección de ciclos sólo puede llevarse a cabo cuando un argumento refcount se decrementa a un valor que no sea cero. En segundo lugar, en un ciclo de recolección de basura, es posible averiguar qué partes son basura comprobando si se puede decrementar en uno sus refcount, para después comprobar cuáles zval poseen un refcount de cero. Algoritmo de recolección de basura Para evitar llamar a la comprobación de ciclos de basura en cada posible decremento de un refcount, el algoritmo lo que hace es pasar todas las posibles raíces (zvals) al "buffer raíz" (marcándolos en "púrpura"). También se asegura de que cada raíz de basura sólo finaliza una vez en el buffer. Únicamente cuando el buffer raíz está completo, comienza el mecanismo de recolección para todos los zval diferentes que haya en su interior. Véase el paso A en la figura anterior. En el paso B, el algoritmo inicia una primera búsqueda en profundidad de todas las posibles raíces en las que decrementa por uno los refcount de los zval que encuentra, asegurándose de que no decrementa dos veces el mismo zval (marcándolo en "gris"). En el paso C, el algoritmo vuelve a llevar a cabo una búsqueda en profundidad dentro de cada nodo raíz, para volver a comprobar el refcount de cada zval. Si ve que el refcount es cero, se marca al zval en "blanco" (azul en la figura). Si es mayor que cero, deshace el decremento con una búsqueda en profundidad partiendo de ese punto, y se le vuelve a marcar en "negro". En el último paso (D), el algoritmo recorre el buffer raíz eliminando las raíces zval que haya, y a la vez, comprueba qué zvals se han marcado en "blanco" en el paso anterior. Todos los zval marcados en "blanco" se eliminarán. Ahora que ya tiene un conocimiento básico de cómo funciona el algoritmo, volveremos atrás para ver cómo se integra esto en PHP. Por omisión, el recolector de basuras de PHP está habilitado. Hay, sin embargo, una directiva en &php.ini; que permite cambiar esto: zend.enable_gc. Cuando el recolector de basura está habilitado, el algoritmo que busca ciclos, tal y como se ha descrito arriba, se ejecuta cada vez que se llena el buffer raíz. Éste tiene un tamaño fijo de 10.000 raíces posibles (se puede modificar esto cambiando la contante GC_ROOT_BUFFER_MAX_ENTRIES en Zend/zend_gc.c del código fuente de PHP, y recompilando PHP). Cuando el recolector de basuras se deshabilita, no se ejecutará el algoritmo que busca ciclos. Sea como fuere, las posibles raíces seguirían registrándose en el buffer raíz, sin importar si el mecanismo recolector de basuras está habilitado en la configuración o no. Si estando deshabilitado el mecanismo recolector de basuras se llenara el buffer raíz de posibles raíces, no se registraría al resto de raíces posibles, por lo que no llegarían a ser analizadas por el algoritmo. Si fueran parte de un ciclo de referencia circular, nunca se liberarían y podrían provocar una fuga de memoria. La razón por la que se registran las posibles raíces estando deshabilitado el mecanismo es porque es más rápido registrarlas que comprobar en cada una de ellas si el mecanismo está habilitado. Sin embargo, el recolector de basuras y el propio mecanismo de análisis, sí puede conllevar una cantidad de tiempo considerable. Ademas de poder cambiar la configuración zend.enable_gc, también es posible habilitar o deshabilitar el mecanismo recolector de basura llamando a gc_enable o gc_disable respectivamente. La llamada a estas funciones tiene el mismo efecto que habilitar o deshabilitar el mecanismo en la propia configuración. También es posible forzar la recolección de ciclos incluso sin que esté lleno el buffer raíz. Para hacer esto, se puede usar la función gc_collect_cycles. Esta función devuelve el número de ciclos que fueron recolectados por el algoritmo. El motivo por el que es posible habilitar o deshabilitar el mecanismo, o iniciar los ciclos de recolección a mano, es porque podría haber determinadas partes de una aplicación que necesiten mucha precisión de tiempo. En estos casos, quizás no se desee que funcione el mecanismo recolector de basuras. Por supuesto, al deshabilitar el recolector de basuras en algunas partes del código, se corre el riesgo de provocar fugas de memoria, ya que algunas raíces podrían no caber en el buffer raíz. Por tanto, lo mas prudente sería llamar a gc_collect_cycles justo después de llamar a gc_disable para que libere la memoria ocupada por posibles raíces ya registradas en el buffer raíz. Esto deja por tanto un buffer vacío, de modo que queda más espacio para almacenar posibles raíces en el tiempo en que el mecanismo recolector de ciclos está deshabilitado. Consideraciones acerca del Rendimiento Como mencionamos en la sección anterior, la recolección de raíces tiene muy bajo impacto en el rendimiento, pero aquí es cuando comparamos PHP 5.2 contra PHP 5.3. Si bien la recolección de posibles raíces comparado con la no recolección, como en PHP 5.2, es más lenta, hay otras modificaciones en tiempo de ejecución en PHP 5.3 que impiden que esta pérdida de rendimiento en particular pueda siquiera apreciarse. Hay dos principales sectores en los que el rendimiento se ve afectado. El primero es el uso reducido de memoria, y mientras que el segunda es la reducción en tiempo de ejecución cuando el mecanismo recolector de basura lleva a cabo la limpieza de memoria. Revisaremos estos dos asuntos. Uso Reducido de Memoria Antes de nada, la razón por la que se implementa el mecanismo recolector de basuras es para reducir el uso de memoria limpiando, una vez que se cumplen las condiciones, las variables de referencias circulares. En la implementación de PHP, esto sucede cuando el buffer raíz está lleno, o cuando se invoca la función gc_collect_cycles. En el gráfico mostrado abajo, se muestra el uso de memoria tanto en PHP 5.2 como en 5.3, excluyendo la memoria base que el propio PHP ocupa al arrancar. Ejemplo de uso de memoria self = $a; if ( $i % 500 === 0 ) { echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n"; } } ?> ]]> Comparación de uso de memoria entre PHP 5.2 y PHP 5.3 En este ejemplo didáctico, estamos creando un objeto en el que una propiedad enlaza de nuevo al propio objeto. Cuando la variable $a del script se reasigna en la siguiente iteración del bucle, típicamente ocurriría una fuga de memoria. En este caso, se fugan dos contenedores zval (el zval del objeto, y el zval de la propiead), pero sólo se encuentra una posible raíz: la variable que se desasignó. Tras 10.000 iteraciones, el buffer se llena (con un total de 10.000 posibles raíces), y se lanza el mecanismo recolector de basura y libera la memoria asociada con esas posibles raíces. Puede apreciarse claramente en el uso de memoria "dentado" de la gráfica para PHP 5.3. Tras las 10.000 iteraciones, el mecanismo libera la memoria asociada a las variables con referencias cíclicas. En este ejemplo, el propio mecanismo no debe hacer un gran trabajo, puesto que la estructura que produce la fuga es extremadamente sencilla. A partir del diagrama, se puede comprobar que el uso máximo de memoria en PHP 5.3 es en torno a 9 Mb, mientras que en PHP 5.2 el uso de memoria no para de aumentar. Reducción en Tiempo de Ejecución El segundo sector en el que el mecanismo recolector de basura influye en el rendimiento es en el tiempo que lleva a éste liberar la memoria "fugada". Para comprobar de cuánto estamos hablando, modificaremos ligeramente el script anterior para tener en cuenta un mayor número de iteraciones, y eliminaremos las figuras de uso de memoria intermedio. Este es el segundo script: Influencia en rendimiento de Recolector de Basuras self = $a; } echo memory_get_peak_usage(), "\n"; ?> ]]> Ejecutaremos dos veces este script, una con el ajuste zend.enable_gc habilitado, y en la otra deshabilitado: Ejecutando el script anterior En la máquina de ejemplo, el primer comando parece llevar en torno a 10,7 segundos, mientras que al segundo comando le lleva 11,4. Esto es un incremento de en torno al 7%. Sin embargo, el uso máximo de memoria del script se ha reducido en un 98%, pasando de 931Mb a 10Mb. Esta prueba no es muy científica, ni siquiera representativa de aplicaciones reales, pero demuestra que el uso de memoria se beneficia del mecanismo recolector de basuras. Lo interesante es que para este script en particular el incremento es siempre del 7%, mientras que el ahorro de memoria aumenta a medida que se encuentran más referencias cíclicas en la ejecución del script. Estadísticas Internas de PHP del Recolector de Basuras Todavía es posible dar más información sobre cómo funciona el mecanismo recolector de basuras dentro de PHP. Pero para hacerlo, será necesario recompilar PHP para habilitar el código de análises y de recopilación de datos. Se tendrá que asignar a la variable de entorno CFLAGS el valor -DGC_BENCH=1 antes de ejecutar ./configure con las opciones deseadas. La siguiente secuencia muestra cómo hacerlo: Recompilando PHP para habilitar el análisis del Recolector de Basuras Al ejecutar el ejemplo que vimos arriba con el nuevo binario de PHP que hemos creado, veremos que se muestra el siguiente resultado tras la ejecución de PHP: Estadísticas de Recolección de Basuras Las estadísticas más informativas son las que se muestran en el primer bloque. Puede comprobarse que el mecanismo recolector de basuras se ejecutó 110 veces, y en total, se liberaron más de 2 millones de ubicaciones en memoria durante esas 110 ejecuciones. Puesto que el mecanismo recolector de ciclos se ha ejecutado al menos una vez, el "pico del buffer raíz" es siempre 10.000. Conclusión En general el recolector de basuras de PHP sólo provocará un retraso cuando el algoritmo recolector de ciclos funcione, mientras que en scripts normales (más pequeños) no habrá un impacto real en el rendimiento. Sin embargo, en el caso en el que el mecanismo recolector de ciclos se ejecute para scripts normales, la reducción de memoria permitirá que puedan funcionar más scripts concurrentemente en el servidor, ya que en total no utilizarán mucha memoria. Los beneficios son más evidentss en scripts de larga duración, tales como grandes suits de pruebas o scripts demonios. También en las aplicaciones PHP-GTK, que generalmente suelen ejecutarse durante más tiempo que scripts para la Web; el nuevo mecanismo marcará la diferencia en cuanto a las fugas de memoria progresivas en el tiempo.