Publi

Comparativa de acceso rápido y seguro a un elemento de un array unidimensional [PHP]

477292813_c913243065_o

Nota: Si usas algún Framework PHP, lo más seguro que estas funciones no te sirvan, ya que muchos Frameworks suelen tener cosas parecidas, más o menos completas que hacen lo mismo. Si eres de los que trabajan “a pelo”, estás creándote tu propio framework, tal vez te interese todo esto.

Acceso simple a un elemento de un array

Esto es una tarea sencilla en PHP, sólo tenemos que poner entre corchetes la clave a la que queremos acceder y podremos acceder a su valor:

1
2
3
<?php
$miArray['precio']=12.34;
echo $miArray['precio'];

Aunque, el problema viene cuando dicha clave no existe, veremos una notificación de PHP indicando que el índice especificado no existe:

PHP Notice: Undefined index: precio in /home/pruebas/test.php on line 3
PHP Stack trace:
…..

No pasa nada, aquí podemos hacer varias cosas, desactivas las notificaciones de PHP, cosa que en producción debemos hacer, pero no en desarrollo. Poner una @ delante del nombre de variable:

1
2
3
<?php
//$miArray['precio']=12.34;
echo @$miArray['precio'];

que es algo así como un “no te quejes por esto”, o verificar la existencia de la clave con anterioridad que, para mí es la forma más elegante y que, a la larga, nos permite más cosas:

1
2
3
<?php
if (isset($miArray['precio']))
  echo $miArray['precio'];

Bueno, vamos a hacer una pequeña comparativa, tal y como hice hace casi 5 años con los valores por GET, pero esta vez con PHP5.5 (una versión más o menos actual y estable, aunque tengamos PHP 5.6 o HHVM, en muchos lugares se usan versiones algo más antiguas.

A priori, sabemos que cuando en PHP se genera un error, puede llegar a ralentizar mucho el código (aunque una prueba, puede que no retrase mucho el código, nunca hacemos sólo una, y cuando se trata de valores de base de datos, o muchas iteraciones, la cosa se complica.

Así que, vamos a realizar una serie de tests, para ver cómo se comporta PHP en diferentes escenarios cuando queremos acceder un valor de un array cuando existe dicho valor, y cuando no existe. Los tests serán los siguientes:

Acceso simple

Diremos que es un acceso simple cuando utilicemos la forma más primitiva de PHP para acceder a elementos de un array. Como hemos visto antes:

1
2
3
<?php
//$miArray['precio']=12.34;
echo $miArray['precio'];

Dependiendo del test, iremos cambiando la clave del array, y quitaremos el comentario del valor para que el valor exista o no exista.
Como se generarán mensajes de error, dichos mensajes serán redirigidos a /dev/null para no mostrarlos por pantalla (si los mostramos por pantalla sí que puede tardar mucho).

Acceso simple con error_reporting(0)

Desactivaremos el informe de errores de PHP poniendo error_reporting(0) al principio del código. Con esto, no deberían generarse los textos de los errores.

Acceso simple con @

Como hicimos antes,

1
2
3
<?php
//$miArray['precio']=12.34;
echo @$miArray['precio'];

poniendo una @ delante del array, le decimos a PHP que no saque los errores correspondientes a dicha operación.

if (isset(…))

Como isset() no es una función, sino una construcción propia de PHP, será una forma rápida de evaluar si un valor existe o no. En la comparativa tendremos el problema (o la ventaja) de que, cuando el valor no existe, no se evaluará nada más, sólo ser hará el isset():

1
2
3
4
<?php
//$miArray['precio']=12.34;
if (isset($miArray['precio']))
   echo @$miArray['precio'];

Operador ternario

Utilizaremos isset, pero, intentaremos condensar en una sola línea todas las acciones, y así escribir menos. Además, en la misma orden tenemos un valor alternativo para cuando el elemento del array no existe. Eso está muy bien para tener valores por defecto de forma rápida:

1
2
3
<?php
//$miArray['precio']=12.34;
echo (isset($miArray['precio']))?$miArray['precio']:'';

Función av() parámetros por valor

Utilizaremos una función, av(). Y pasamos los parámetros por valor. La razón es simplificar un poco más cada una de las llamadas, y es que, dada la frecuencia con la que podemos utilizar esta comprobación de variable, hacer un if (isset()) o incluso utilizar el operador ternario, aunque sea rápido con respecto a su ejecución, tal vez no lo sea tanto con respecto a su programación (podemos utilizar macros en nuestro editor, pero tendremos que escribir la clave y el array dos veces, y eso no es muy útil.
En la función especificaremos directamente el valor por defecto

1
2
3
4
5
6
7
function av($arr, $key, $default=null)
{
   return (isset($arr[$key]))?$arr[$key]:$default;
}

//$miArray['precio']=12.34;
echo av($miArray, 'precio');

Función av(), Array por referencia

Ahora, nos queda jugar con el paso de los argumentos, para ver si se consigue más o menos rendimiento haciendo esos pasos por referencia. En teoría, puede parecer que sí, pero nunca se sabe:

1
2
3
4
5
6
7
function av(&$arr, $key, $default=null)
{
   return (isset($arr[$key]))?$arr[$key]:$default;
}

//$miArray['precio']=12.34;
echo av($miArray, 'precio');

Función av(). Clave por referencia

Ahora probamos la clave por referencia, a ver qué pasa:

1
2
3
4
5
6
7
8
function av($arr, &$key, $default=null)
{
   return (isset($arr[$key]))?$arr[$key]:$default;
}

//$miArray['precio']=12.34;
$clave = 'precio';
echo av($miArray, $clave);

Este método no será muy útil, porque la clave debe poder ser especificada directamente al llamar la función, como hemos estado haciéndolo en todos los ejemplos anteriores. Ahora, la clave debe estar en una variable para poder pasarla. Sólo será un resultado más en el test.

Función av(). Clave y array por referencia

Ahora, los dos parámetros por referencia, como antes. Sólo será un resultado en el test, para ver cómo se comporta porque no sería útil en el mundo real.

1
2
3
4
5
6
7
8
function av($arr, &$key, $default=null)
{
   return (isset($arr[$key]))?$arr[$key]:$default;
}

//$miArray['precio']=12.34;
$clave = 'precio';
echo av($miArray, $clave);

Función av() con if…else con parámetros por valor

La función av() anterior estaba hecha con el operador ternario, aunque, si vemos que hay mucha diferencia entre el test del if…else anterior y el del operador ternario, tal vez esos cambios se vean reflejados aquí también.

1
2
3
4
5
6
7
8
9
10
function av($arr, $key, $default=null)
{
        if (isset($arr[$key]))
                return $arr[$key];
        else
                return $default;
}

//$miArray['precio']=12.34;
echo av($miArray, 'precio');

Función av() con if…else con array por referencia

Como antes, vamos a ver si pasando por referencia ciertos valores podemos conseguir algo de rendimiento.

1
2
3
4
5
6
7
8
9
10
function av(&$arr, $key, $default=null)
{
        if (isset($arr[$key]))
                return $arr[$key];
        else
                return $default;
}

//$miArray['precio']=12.34;
echo av($miArray, 'precio');

Función av() con if…else con clave por referencia

Como antes, sólo una opción más para la comparativa. No podremos usarla a menudo, pero para ver si tiene más rendimiento o no.

1
2
3
4
5
6
7
8
9
10
11
function av($arr, &$key, $default=null)
{
        if (isset($arr[$key]))
                return $arr[$key];
        else
                return $default;
}

//$miArray['precio']=12.34;
$clave = 'precio';
echo av($miArray, $clave);

Función av() con if…else con array y clave por referencia

Por último, array y clave por referencia. A ver qué pasa.

1
2
3
4
5
6
7
8
9
10
11
function av(&$arr, &$key, $default=null)
{
        if (isset($arr[$key]))
                return $arr[$key];
        else
                return $default;
}

//$miArray['precio']=12.34;
$clave = 'precio';
echo av($miArray, $clave);

Las pruebas

Para hacer las pruebas se han hecho scripts con diferentes características:

  • Una clave alfanumérica existente. El elemento existe. La clave alfanumérica es un md5: “1c7d02afc6e3c6a7dd59bcdf2fefb387”
  • Una clave numérica existente. Clave 999
  • Una clave alfanumérica no existente. La clave es un md5: “1c7d02afc6e3c6a7dd59bcdf2fefb387”
  • Una clave numérica no existente. Clave 999
  • Claves numéricas consecutivas, no existentes
  • Claves de texto con uniqid(), no existentes
  • Claves numéricas consecutivas, dando valor antes de leer
  • Claves de texto con uniqid(), dando valor antes de leer

Debemos ser conscientes de que uniqid(), introducirá un tiempo de retardo, por lo que, los números tendrán sentido dentro de la prueba, no podremos compararlo con los demás tests.

Los scripts realizados realizan 1000000 (un millón) de peticiones del elemento que vamos a buscar dentro de un bucle for. for ($i=0; $i$ time php script.php

Con lo que obtenemos el tiempo que se ha tardado en la ejecución. Se han hecho tres pruebas con cada opción y se anota la media de los tres tiempos. Con esto sacaremos una tabla y una gráfica.
El tiempo, es muy dependiente de mi CPU. La CPU con la que he hecho las pruebas es una Intel(R) Core(TM) i5-2450M CPU @ 2.50GHz puede que si se replican los tests con otra CPU salgan resultados diferentes. Y la versión exacta de PHP utilizada es la 5.5.19 con Xdebug v2.3.2

Resultados

Dividiré los resultados en dos: por un lado tendremos el caso en el que las claves existen en el array, por lo que no se producirá un error interno (que es lo que en principio nos causará las pérdidas de tiempo), y por otro lado, los casos en los que la clave no existe (se han probado con arrays vacíos (la implementación de los arrays en PHP es en teoría una tabla hash, en el peor de los casos, la búsqueda de un elemento, aunque se nota, el array tiene que tener muchos elementos para que sea muy significativo.

Nota: El tiempo siempre está en segundos.

Cuando la clave existe

Clave alfanumérica existente

Clave numérica existente

Acceso simple

0,501

0,481

Acceso simple. Error_reporting(0)

0,477

0,485

Acceso simple. Con @

1,028

1,076

if (isset($array[$clave])) …

0,673

0,664

Operador ternario

0,755

0,747

function av. Por valor

2,683

2,543

function av. Array por referencia

2,655

2,593

function av. Clave por referencia

2,744

2,6

function av. Array y clave por referencia

2,706

2,6

function av con if…else

2,531

2,42

function av con if..else y array por referencia

2,647

2,459

function av con if..else y clave por referencia

2,565

2,487

function av con if…else Array y clave por referencia

2,597

2,56

En la gráfica, las funciones av() se han representado solo por valor.

Clave alfanumérica existente

Clave numérica existente


Los números son muy parecidos tanto para claves numéricas como alfanuméricas, siempre que existan. Sabemos que estos números dependen de muchos factores, porque en un ordenador hay cosas que nunca están quietas, y lo mismo durante un test se inició un servicio programado que ralentizó un poco un test… de todas formas, para una misma prueba, tanto claves numéricas como alfanuméricas existentes desembocan en valores similares.

Lo más rápido, como es de esperar, es el acceso normal $array[$clave], aunque si comprobamos primero la existencia del valor con isset() ya sea con if (isset($array[$clave])) como con un operador ternario, no hay problema en tiempo. Ya utilizar una función propia es más pesado, y es normal, PHP no deja de ser un lenguaje interpretado y distingue entre funciones y construcciones del lenguaje (echo, die, isset y algunas más). Las construcciones, como isset() son muy rápidas, pero una función requiere cambios de contexto, procesamiento de argumentos, y muchas cosas que damos por hechas que en realidad consumen tiempo.

También debemos saber que estamos haciendo 10^6 (un millón) de iteraciones, por lo que, dos segundos no es tanto tiempo. Si tenemos la necesidad de tratar tanta información, igual PHP no es el mejor lenguaje para ello.

Cuando la clave existe

Aquí es donde está el problema, cuando la clave no existe, PHP enviará un error, así veremos cómo el acceso simple tardará muchísimo más (enviando notices), aunque yo los he ignorado para no ralentizar el sistema mostrando la salida, los notices se generan y consumen tiempo, y mucho.
Tanto con error_reporting(0) como con @ el tiempo crece mucho en comparación con el caso en el que las claves existen.

Por otro lado, he incluido la generación de claves con uniqid(), para que sean claves diferentes y alfanuméricas, pero uniqid() tarda mucho tiempo en generar un millón de claves, por lo que he aislado el tiempo de generación (75.221s) y lo he restado al tiempo de los accesos, para obtener una idea de cuánto tarda en acceder, aunque el tiempo de las generaciones puede variar, con esto nos hacemos una idea.

Aquí los resultados

Misma clave alfanuméricaMisma clave numéricaClave numérica consecutivaClave uniqid()
Acceso simple23,75524,3723,89270,554
Acceso simple. Error_reporting(0)1,2081,2081,2958,467
Acceso simple. Con @1,8051,8561,93911,843
if (isset($array[$clave])) …0,4840,4840,5042,34
Operador ternario0,7680,7680,8022,149
function av. Por valor2,4432,362,22810,742
function av. Array por referencia2,3792,3932,24910,488
function av. Clave por referencia2,5032,4012,24710,966
function av. Array y clave por referencia2,5552,4452,25110,756
function av con if…else2,3012,282,19610,406
function av con if..else y array por referencia2,3482,2462,1949,679
function av con if..else y clave por referencia2,3652,3252,11910,449
function av con if…else Array y clave por referencia2,4232,3832,24910,367

Y mostramos también las gráficas como para los valores conocidos.

Misma clave alfanumérica

Misma clave numérica

Clave numérica consecutiva

Clave uniqid()


Como hemos visto, cuando el valor no existe, lo mejor es comprobar antes su existencia, de esta forma, no intentamos acceder al elemento del array, no generamos error y no causamos esperas no deseadas. Es más, si comprobamos, el valor con anterioridad, estaremos utilizando mejores técnicas en nuestro programa, utilizando valores por defecto cuando los elementos no existan, así inicializando todas las variables y tomando decisiones si los elementos no existen.

Utilizar error_reporting(0), como se supone que debe estar en producción no es la solución, porque en tiempo de desarrollo, debemos estar atentos a todos los errores, y pueden surgir muchos, que se pueden ignorar, sí, pero es incómodo trabajar con errores constantemente en pantalla.

Poner una @ delante de la comprobación también vale, aunque es más lento que el error_reporting(0), aunque como programadores, debemos ignorar los errores en la menor cantidad de sitios posible, si nos acostumbramos a poner arrobas por el código, estamos expuestos a escribir un galimatías de cuidado. Nuestra misión no es ignorar errores, es no producirlos, o al menos, controlarlos.

El tema de utilizar una función, yo la he llamado av() es, sobre todo, para ahorrar código, porque escribir if (isset($array[$clave])) $val = $array[$clave]; es muy largo, y tenemos que repetir $array[$clave] dos veces, lo que puede dar lugar a errores… ¡ somos humanos ! y más de una vez, el primero se escribe bien y el segundo mal, o viceversa. Tener una función, aunque hace el código un pelín más lento, puede ayudarnos a evitar errores y a programar más rápidamente, al ponerle un nombre tan pequeño, se escribe en muy poco tiempo y está muy bien cuando te acostumbras. Aún así, utilizar una función, da mucho mejor resultado que acceder a un elemento no existente de un array devolviendo errores.

Anexo: Novedad en PHP7!

Bueno, a día de hoy nos encontramos ante la RC3 de PHP7.0.0, aunque no sea 100% estable y definitiva, podemos ir experimentando con ella. Una gran novedad de esta versión es que han incorporado el null-coalesce operator, no sé muy bien cómo traducirlo al español. Con esta versión de PHP, y este nuevo operador. ¿ Qué tiempos de acceso tenemos en los casos anteriores ?

Clave numérica existenteClave alfanumérica existenteMisma clave alfanuméricaMisma clave numéricaClave numérica consecutivaClave uniqid()
Null-coalesce0,140,1450,1490,1430,141,684

Lo primero, PHP7, está mucho mejor optimizado, sólo hace falta ver los tiempos, y este operador es una buenísima forma de optimizar nuestro código porque directamente sustituye al isset(), es más. es capa de acceder a múltiples elementos de un array multidimensional, es decir:

1
2
<?php
echo $opciones['database']['mysql']['username'] ?? "No hay usuario";

Con el operador, podemos sustituir a:

1
2
3
4
5
<?php
  if ( (isset($opciones['database'])) && (isset($opciones['database']['mysql'])) && (isset($opciones['database']['mysql']['username'])) )
     echo $opciones['database']['mysql']['username'];
  else
     echo "No hay usuario";

Ahora bien, ¿ qué método prefieres tú para acceder al valor de un array ?
Foto: Mitchel Laurren-Ring (Flickr-CC)

También podría interesarte....

Leave a Reply