19 de marzo de 2011

[PHP] Arrays pasados por parámetro ¿copias o referencias? (2/2)

conjuntoEntradas() {
  [PHP] Arrays pasados por parámetro ¿copias o referencias? (1/2)
  [PHP] Arrays pasados por parámetro ¿copias o referencias? (2/2)
}


El rendimiento

Hasta aquí podía apaliar una de mis inquietudes, que era el nivel de integridad de los atributos de un objeto para cada una de las maneras de tratar los parámetros de los métodos y su asignación a estos, pero todavía me quedaba otra pendiente, que era saber el rendimiento de cada una de estas formas.

Ya se que en el rendimiento de una aplicación están involucrados muchos factores y cuando tu escribes un código, el orden de ejecución de las instrucciones puede ser alterado por el compilador con el objetivo de optimizar el código para una ejecución más eficiente, no obstante siempre que programa y me encuentro delante de los ojos que el orden de ciertas instrucciones podría ser cambiado, a priori por una más optima, si no me aleja enormemente de lo que estoy haciendo, hago el refactoring, o sino me lo marco con @todo; ya sé que ha veces esto puede ser un poco de maniático o de paranoico, pero es algo intrínseco en mi personalidad junto con la concepción de que las pequeñas cosas forman el todo, por lo que si ese pequeño cambio puede llegar a optimizar una mil millonésima parte de la ejecución, lo aplico de todas formas, siempre pienso “mejor eso que nada”.

Bueno dejando de lado mis paranoias, os dejo la modificación del código de la prueba anterior de manera que me permitiera analizar la carga de ejecución de cada manera de pasar parámetros y de asignación de estos atributos.

class A {
    private $arrValor;
    private $arrValorToRef;
    private $arrReferencia;
    private $arrRefToRef;
    

    public function setArrayByValue(array $setArr) {
        $this->arrValor = $setArr;            
    }

    public function setArrayByValueToRef(array $setArr) {
        $this->arrValorToRef = &$setArr;            
    }

    public function setArrayByRef(array &$setArr) {
        $this->arrReferencia = $setArr;
    }

    public function setArrayByRefToRef(array &$setArr) {
        $this->arrRefToRef = &$setArr;        
    }

}


$numIt = 1000000;
$arr = array('key0' => 'value0');
$objA = new A();

for ($it = 0; $it < $numIt; $it++) {
    $objA->setArrayByValue($arr);
    $objA->setArrayByValueToRef($arr);
    $objA->setArrayByRef($arr); 
    $objA->setArrayByRefToRef($arr);
}

La modificación del script de prueba está clara, solo he dejé en los métodos de las clases declarada la asignación del parámetro al atributo, que es lo que quería comparar a nivel de coste de ejecución, y en la ejecución del script iterar unas cuantas vueltas dentro de un bucle la llamada a los cuatro métodos.

Para saber el coste de ejecución lo que hice es ejecutar el script en un profiler, concretamente el de xdebug; hice dos pruebas, una con 1.000.000 iteraciones y otra con 3.000.000 de iteraciones; ¿porqué tantas? para que el profiler pudiese medir con mayor precisión la diferencia entre cada llamada y ¿porqué dos veces?, para ver si los valores se obtenidos daban valores diferenciales similares entre las llamadas de los distintos métodos.

El resultado que obtuve fue el siguiente:

 Gráfico de llamadas a los métodos con 1.000.000 de iteraciones


Tabla de porcentaje de tiempo de carga sobre el total de llamada a los métodos con 1.000.000 de iteraciones

Gráfico de llamadas a los métodos con 3.000.000 de iteraciones

Tabla de porcentaje de tiempo de carga sobre el total de llamada a los métodos con 3.000.000 de iteraciones

Después de los resultados obtenidos a partir de los ficheros generados por el profiler con la ejecución del script, y haber sacada los gráficos y las tablas anteriores de cada ejecución con el visor kcachegrind, me decidí a poner los valores en un hoja de cálculo para poder analizar de manera más cómoda los resultados.

 Valores de los resultados, medias y diferencias entre ellos

Como podemos observar, y era de esperar, el método más eficiente es pasar el parámetro por referencia y asignar la referencia al atributo, ya que no se hace ninguna copia del array pasado por parámetro. Lo más curioso es que el siguiente más eficiente es el método que se pasa el parámetro por valor y se asigna por valor (setArrayByValue), estando por delante de los otros dos, yo, si no hubiese hecho esta prueba, hubiese intuido que éste sería el menos eficiente, ya que debería estar haciendo dos copias del array, una cuando lo pasa por parámetro y la otra cuando lo asigna a la variable, mientras que los otros dos deberían tener costes similares ya que hacen una copia del array.

Conclusiones

Después de lo visto tengo claro que para mantener la integridad y el encapsulamiento de los atributos de una clase en PHP, tengo que pasarlo por parámetro y asignarlos por parámetro, ya que si todo son referencias (parámetro y asignación al atributo), como ya suponía, los valores pueden ser modificados desde fuera; además después, de lo observado, también tengo claro que de nada sirve pasar el parámetro o asignarlo al atributo por referencia, ya que no introduce optimización ninguna, es más, el rendimiento disminuye, ya que la ejecución es más costosa.
Además de todo esto, me puedo despreocupar de que ciertas referencias queden apuntando espacios de memoria liberados, ya que el interprete garantiza la destrucción de los espacios de memoria, solo cuando no haya ninguna variable que pueda referenciar a éste.


Hasta la próxima enfermos.

12 de marzo de 2011

[PHP] Arrays pasados por parámetro ¿copias o referencias? (1/2)

conjuntoEntradas() {
  [PHP] Arrays pasados por parámetro ¿copias o referencias? (1/2)
  [PHP] Arrays pasados por parámetro ¿copias o referencias? (2/2)
}


Introducción

Realizando mis jugueteos con la programación en PHP, una de las cosas que necesitaba hacer era pasar arrays a objetos de clases creadas por mi mismo; en algunas de estas clases he considerado oportuno mantener los valores que se pasan por parámetro a través de un array en uno de los atributos de la clase debido a que me pueden ser necesarios para realizar operaciones futuras con o sobre esos valores.

Hasta aquí nada nuevo, ya que es una práctica muy utilizada; pero a mí, que no soy, para nada, un “ninja” del PHP, me hizo pensar que realmente sucedía al pasar un array por parámetro de las distintas maneras que permite el lenguaje, valor o referencia y la manera de asignarlo a un atributo, valor o referencia. Por mis conocimientos, sabía los distintos comportamientos que podían suceder pero, al no estar seguro de cual sería el que se aplicaría en PHP, y si teniendo lo muy claro en otros lenguajes donde tu puedes controlar las posiciones de memoria con referencias y, con mayor control, con punteros, como es el caso de C y su hermano C++.

Como después de buscar y revisar un poco la documentación y algunos enlaces de cuatro búsquedas por Internet no me continuaba sin quedar del todo claro, decidí hacer un script PHP muy simple que me permitiera observar el comportamiento del lenguaje antes esta común situación.
La pequeña prueba
El script que cree, hace lo siguiente:
  1. Declara una clase con:
    • Tres atributos; uno para almacenar el parámetro que se pasará a a cada uno de los tres métodos declarados.
    • Tres métodos, cada uno recibe un array por parámetro, pero uno lo recibe por valor y los otros dos por referencia; la diferencia entre los dos métodos que reciben el array por referencia está en su interior, además de que cada uno asigna el array a un atributo distinto, uno lo asigna el valor y otro asigna la referencia.
      Cada método, además de asignar el array a un atributo, realiza una modificación (añade un nuevo valor) en el array pasado por parámetro.
    class A {
        private $arrValor;
        private $arrValorToRef;
        private $arrReferencia;
        private $arrRefToRef;
        
    
        public function setArrayByValue(array $setArr) {
            $this->arrValor = $setArr;
            $setArr['keyMarkClassAValue'] = 'valueMarkClassA';
            $this->arrValor['keyClassA'] = 'valueClassA';
        }
    
        public function setArrayByValueToRef(array $setArr) {
            $this->arrValorToRef = &$setArr;
            $setArr['keyMarkClassAValueToRef'] = 'valueMarkClassA';
            $this->arrValor['keyClassA'] = 'valueToRefClassA';
        }
    
        public function setArrayByRef(array &$setArr) {
            $this->arrReferencia = $setArr;
            $setArr['keyMarkClassARef'] = 'refMarkClassA';
            $this->arrReferencia['keyClassA'] = 'refClassA';
        }
    
        public function setArrayByRefToRef(array &$setArr) {
            $this->arrRefToRef = &$setArr;
            $setArr['keyMarkClassARefToRef'] = 'refToRefMarkClassA';
            $this->arrRefToRef['keyClassA'] = 'refToRefClassA';
        }
    
    }
    
  2. Declara una array con un valor en su interior, el que se utilizará para pasarlo a los tres métodos de un objeto, de la clase declarada, creado.
    $arr = array('key0' => 'value0');
    
  3. Se van realizando dumpeos del array utilizado como parámetro y del objeto a medida que se van ejecutando los métodos para observar comportamiento.
    print "Contenido del array \$arr antes de asignarlo a la clase: \n";
    var_dump($arr);
    print "\n";
    
    $objA = new A();
    $objA->setArrayByValue($arr);
    
    print "Contenido del array \$arr despues de asignarlo a la clase por valor: \n";
    var_dump($arr);
    print "\n";
    
    $objA->setArrayByRef($arr);
    
    print "Contenido del array \$arr despues de asignarlo a la clase por referencia: \n";
    var_dump($arr);
    print "\n";
    
    $objA->setArrayByValueToRef($arr);
    $objA->setArrayByRefToRef($arr);
    
    print "Contenido del object de A despues de asignarle el array (valor y referencias): \n";
    var_dump($objA);
    print "\n";
    
    $arr['newKey'] = 'newValue';
    
    print "Contenido del object de A despues de ejecutar \$arr['newKey'] => 'newValue': \n";
    var_dump($objA);
    print "\n";
    
    unset($arr);
    
    print "Contenido del object de A despues de ejecutar un unset() del array: \n";
    var_dump($objA);
    print "\n";
    

El encapsulamiento

Con la salida que genera el script podemos llegar a ciertas conclusiones sobre lo que sucede con la asignación del array pasado por parámetro a los atributos del objeto, con las distintas maneras que se permiten, y ver si realmente el atributo del objeto mantiene una referencia a esa posición de memoria o copia el valor, desde el punto de vista de utilización habitual del lenguaje, es decir sin buscar saltarnos lo establecido, ya que al referirme que se puede estar copiando el valor del array a otra posición de memoria, me refiero a la percepción que se tiene como desarrollador, dejando de lado el funcionamiento interno del interprete, ya que este podría estar optimizando el rendimiento de la ejecución demorando la copia de los valores a una nueva posición de memoria hasta el momento antes de que alguno de los valores de la variable sufra una modificación, garantizando e esta manera la integridad de los valores de cada variable.

El resultado obtenido es el siguiente:
  1. Parámetro por valor y asignación por valor. Variable pasada por parámetro y atributo utilizan su propio espacio de memoria.
  2. Parámetro por valor y asignación por referencia. Variable pasada por parámetro es copiada a un nuevo espacio de memoria ya que cuando se asigna por referencia al atributo (su dirección de memoria), los cambios en los datos de cada variable son independientes.
  3. Parámetro por referencia y asignación por valor. Variable pasada por parámetro, supuestamente no es copiada, ya que se está pasando por referencia, pero al asignarla al atributo es cuando se realiza una copia de los datos de esta variable a nuevo espacio de memoria que será donde apuntará el atributo al que se está asignando.
  4. Parámetro pasado por referencia y asignación por referencia. No hay copias de los datos a nuevos espacios de memoria por lo que, tanto la variable pasada por parámetro como el atributo apuntan al mismo espacio de memoria.

Con las cuatro pruebas anteriores se puede deducir que las referencias en PHP son tratadas como en otro lenguaje, como C/C++ (donde el control de la memoria depende totalmente del desarrollador), pero con algo menos de liberta, ya que el lenguaje controla el número de variables que apuntan al mismo espacio de memoria y bajo la existencia de una sola, se imposibilita su liberación impidiendo que una variable haga referencia a un espacio de memoria destruido (caso b), además de también imposibilitar la liberación solicitada explícitamente (unset).

Esto ya me permitía saber que sucedía, en PHP, con los arrays, cuando son utilizados como parámetro en una función/método y en las asignaciones a nuevas variables, pudiendo decidir como hacerlo, si por valor o por referencia, en función de lo que realmente quiero hacer con ellos.
Este tipo de dudas siempre se me plantean con los lenguajes, donde el control de la memoria se hace automáticamente mediante un garbage collector, ya que en algunos, como Java, que aunque no existe el concepto de referencia y valor, si que existen los immutable objects en el mismo JDK, provocando un comportamiento distinto, en objetos de ciertas clases, delante de estos casos.

Hasta la próxima enfermos.