Serializar y comprimir arrays en PHP y JSON

Serializar un array con PHP y JSON

array PHP Cuando hice el buscador interno para este sitio lo basé en un índice de palabras clave almacenadas en un array. Realmente hay dos archivos de índices, uno es indice-web.txt con un tamaño actual de 166 KB y el otro es indice-claves.txt que pesa 87 KB. Están almacenados en archivos de texto como arrays serializados. Se trata de convertir los arrays en una cadena de texto plano para poder guardarlo en disco y luego extraerlo. Para la serialización se usa la función de PHP serialize(). Por ejemplo, supongamos este array sencillo:

$arr = array("naranjas","manzanas","peras");

Lo serializamos con la función anterior y obtenemos esta cadena de texto:

a:3:{i:0;s:8:"naranjas";i:1;s:8:"manzanas";i:2;s:5:"peras";}

También podemos hacer esa serialización usando la notación JSON, con la función de PHP json_encode():

["naranjas","manzanas","peras"]

La serialización JSON es ideal para enviar arrays al navegador del usuario y reconvertirlas en el navegador con Javascript. Pero también ambas cadenas podemos guardarlas en un archivo de texto para en un momento posterior recuperarlas con unserialize() y json_decode() respectivamente, devolviéndonos los arrays originales. Me interesa en este caso como almacenar un array en el servidor con el índice de palabras claves del buscador interno. Y buscar la forma más rápida de recuperar ese array.

Se observa que la serialización JSON tiene una longitud menor, por lo que podría pensarse que sería más rápida que la serialización normal. Además también podríamos pensar que si comprimimos los datos obtendríamos una mejora.

Comprimir arrays con PHP

Hay un buen conjunto de recursos para comprimir datos con PHP. Para hacer estas pruebas he usado la librería Zlib de PHP. Para comprimir la cadena serializada he usado gzdeflate() con un nivel máximo de compresión (9). Se descomprime con la función gzinflate(). Son apropiadas si no vamos a enviar estos datos fuera, es decir, sólo vamos a comprimir y descomprimir los mismos datos en el servidor, pues estas funciones no añaden cabeceras o controles de paridad al final, como podría hacerlo gzencode() en el formato GZIP, el formato ZIP u otros.

Para los archivos de índices serializados con serialize() tenemos estos datos:

ArchivoOriginalComprimidoTasa compresión
indice-web.txt165 KB41 KB75%
indice-claves.txt87 KB21 KB76%

Si serializamos con json_encode() los archivos originales pesan aún menos (130 KB y 41 KB), ocupando mucho menos los comprimidos.

ArchivoOriginalComprimidoTasa compresión
indice-web.txt130 KB33 KB75%
indice-claves.txt41 KB14 KB66%

Como esos dos archivos se usan en el buscador interno en cada nueva búsqueda, lo lógico sería usar la serialización JSON y la compresión de archivos. Así tendríamos los pesos de 33 KB y 14 KB por lo que tendrían que ser más rapidas las cargas en array de los índices.

Pruebas de la eficiencia en la recuperación de arrays serializados con PHP y JSON

Tener los datos estructurados en arrays y serializados en archivos de texto en lugar de en una base de datos puede ser una carga extra para el servidor. En cada búsqueda hay que cargar los arrays. Pero si el tamaño de los archivos no es muy grande aún pueden usarse en lugar de una base de datos. Podemos optimizar la carga de los arrays pero sin olvidar que todo dependerá del acceso a disco, es decir, ese aspecto será un "cuello de botella" que no podemos salvar directamente desde PHP, aunque si los archivos están comprimidos puede ser lógico pensar que facilitará ese aspecto. ¿Hasta qué punto conviene comprimirlos?

Por otro lado hay diversas formas de leer un archivo de texto con PHP. He visto que tanto la función fread() como file_get_contents() para leer archivos de texto tardan más ocasionalmente en una primera lectura. En las posteriores el tiempo de proceso baja radicalmente debido a que ese archivo se extrae de alguna caché de memoria.

En un apartado del tema sobre buscadores internos que habla sobre los tiempos de lectura de los archivos de índices comenté la necesidad de buscar estructuras de datos y técnicas que nos permitan acceder rápidamente a los índices. Por lo tanto estaba interesado en ver si podía mejorar estos tiempos y para salir de dudas usé el archivo indice-web.txt. Se trata de la serialización de un array multidimensional que guarda los datos del índice (ver estructura de ese array). Hice 4 versiones de ese array:

  • indice-web.txt serializado con serialize()
  • indice-web.txt serializado con json_encode()
  • indice-web.gz serializado con serialize() y comprimido con gzdeflate()
  • indice-web.gz serializado con json_encode() y comprimido con gzdeflate()
Puede ver el código PHP que he usado para hacer esta prueba. Se trata de una página que hace las lecturas y devuelve los resultados de tiempo promediados, como en este ejemplo. La columna "registros" se refiere a que se cuentan el total de entradas del array como prueba de que se cargó el mismo, habiendo para esta prueba un total de 104 registros.

Por otro lado he usado las dos funciones de PHP señaladas para leer: fread() y file_get_contents(). Por lo tanto tenemos 8 variantes posibles de lectura y carga de array:

ExtraerDescomprimirDeserializarTamaño (KB)
file_get_contents()unserialize()165
file_get_contents()json_decode()130
file_get_contents()gzinflate()unserialize()41
file_get_contents()gzinflate()json_decode()33
fread()unserialize()165
fread()json_decode()130
fread()gzinflate()unserialize()41
fread()gzinflate()json_decode()33

Con 100 iteraciones de este grupo (total de 800 lecturas), el promedio de cada variante ordenado creciente fue el siguiente (donde serial se refiere al uso de unserialize() y json al de json_decode()):

EXTRACCION         COMPRIMIDO DECODE  TAMAÑO     TIEMPO (ms)
------------------------------------------------------------
file_get_contents  NO         serial  165 KB     6.52
fread              NO         serial  165 KB     6.56
fread              SI         serial  41 KB      9.26
file_get_contents  SI         serial  41 KB      9.39
file_get_contents  NO         json    130 KB     12.27
fread              NO         json    130 KB     12.34
file_get_contents  SI         json    33 KB      14.04
fread              SI         json    33 KB      14.17

Esta prueba la hice con el servidor Apache+PHP en localhost y aunque los valores absolutos no son interesantes (pues depende de la máquina y las tareas que se estén ejecutando en disco en ese momento) si que puede aclararnos algo la comparación entre valores. En todas las repeticiones de la prueba se observan resultados comparados similares. Se consigue la mayor velocidad de extracción y deserialización con file_get_contents o fread y unserialize(). Es más rápido incluso que con las variantes de archivos comprimidos. La deserialización en JSON resulta la más ineficiente en todos los casos.

Antes de hacer estas pruebas pensaba que si comprimía y serializaba JSON obtendría mejor respuesta. Pues bien, es esa combinación la que produce los peores resultados. Por un lado hay un coste añadido en la descompresión y por otro lado vemos que la deserialización JSON también es más costosa que la de unserialize().