phpser: el serializador binario PHP que supera a igbinary en cachés

phpser es una extensión PHP nativa escrita por Ilia Alshanetsky —autor de igbinary y contribuidor histórico del core de PHP— que actúa como serializador binario pensado desde el principio para cargas de caché. No es un sustituto de propósito general: es una herramienta que acepta la asimetría real de los cachés (se lee mucho más de lo que se escribe) y la convierte en ventaja de diseño.

Los números de los benchmarks del autor hablan por sí solos: codifica entre un 10 y un 70% más rápido que igbinary y decodifica entre un 12 y un 75% más rápido, dependiendo de la forma de los datos.

El problema con serialize() e igbinary

unserialize() sobre datos no confiables es una puerta abierta a inyección de objetos y ataques de gadget-chain. Si los datos vienen de Redis, Memcached o cookies que alguien puede haber manipulado, deserializarlos sin verificar es un riesgo real. igbinary, que lleva una década siendo el estándar, resuelve la velocidad pero no el problema de seguridad: tampoco valida que el payload no haya sido modificado antes de construir objetos.

phpser aborda los dos frentes a la vez.

Cómo funciona por dentro

El diseño se apoya en tres decisiones técnicas que explican la mayor parte de la ganancia de velocidad:

Diccionario de cadenas al inicio del payload

Todas las cadenas únicas del valor a serializar se emiten una sola vez al principio del blob. Durante la decodificación, cada aparición posterior reutiliza la misma zend_string incrementando su refcount en lugar de asignar memoria nueva. Si tienes mil objetos con la propiedad "user_id", solo hay una asignación y 999 incrementos de contador.

Ejecuciones escalares sin etiquetas por elemento

Para arrays numéricos empaquetados (el caso más frecuente en cachés: listas de IDs, resultados de queries), phpser emite un encabezado PACKED_LONGS seguido de enteros en bruto sin etiquetar cada uno. El decodificador entra en un bucle ajustado sin dispatch de tipos en cada iteración, lo que lo hace especialmente rápido con arrays grandes.

Tabla hash preasignada

El serializador escribe el tamaño exacto antes de los datos. Al leer, el decodificador asigna el array con ese tamaño de una sola vez y lo rellena directamente, saltándose los N cálculos de hash, N inserciones incrementales y el posible crecimiento dinámico que haría una implementación naïve. El patrón viene del mundo de Rust (rkyv): «the writer recorded the size so the reader could allocate once and fill».

Rendimiento: la tabla de benchmarks

Forma de datos

Tamaño vs. igbinary

Codificación

Decodificación

packed_1k (array numérico 1.000 ints)

-65%

-70%

-75%

packed_10k (array numérico 10.000 ints)

-63%

-70%

-74%

dto_1000 (1.000 objetos DTO)

-12%

-15%

-18%

rowset_1000 (rowsets heterogéneos)

+1%

-55%

+4% (más lento)

El caso más llamativo: un range(0, 999) ocupa dos tercios menos de espacio y se decodifica en una cuarta parte del tiempo. Los modelos Laravel cacheados como objetos DTO dan un 12% menos de tamaño y un 18% menos de tiempo de decodificación. La única excepción son los rowsets con muy pocas cadenas repetidas, donde el diccionario frontal no amortiza su overhead y la decodificación sale un 4% más lenta.

Modo firmado: seguridad sin trampa

La parte que diferencia a phpser de cualquier otro serializador rápido es el modo firmado. Usa HMAC-SHA256 y verifica la firma antes de decodificar nada. Si el payload ha sido manipulado, nunca llega a construir objetos.

<?php
// Serializar y firmar antes de guardar en caché
$key  = random_bytes(32); // guardar en variable de entorno, nunca en código
$blob = phpser_serialize_signed($valor, $key);
$redis->set('user:42', $blob);

// Al leer, verificar antes de usar
$blob  = $redis->get('user:42');
$valor = phpser_unserialize_signed($blob, $key);

if ($valor === null) {
    // El payload fue manipulado o la clave no coincide.
    // Tratar como fallo de caché y regenerar.
}

La verificación es constante en tiempo (evita timing attacks), y la librería rechaza claves vacías de forma ruidosa: si SECRET no está definido, falla con un error claro en lugar de degradarse silenciosamente a sin-firma.

Para defensa en profundidad, también soporta allowed_classes —la lista blanca de clases que pueden deserializarse— igual que el unserialize() nativo de PHP:

<?php
$valor = phpser_unserialize($blob, ['allowed_classes' => [User::class, Order::class]]);

API completa

pie install iliaal/phpser
<?php
// Sin firma (solo para datos de confianza propia)
$blob  = phpser_serialize($valor);
$valor = phpser_unserialize($blob, ['allowed_classes' => false]);

// Con firma HMAC-SHA256 (recomendado para Redis/Memcached/cookies)
$blob  = phpser_serialize_signed($valor, $clave);
$valor = phpser_unserialize_signed($blob, $clave);

Cuándo usarlo y cuándo no

phpser encaja bien cuando el cuello de botella está en la decodificación de cachés con arrays numéricos grandes u objetos DTO homogéneos —el caso típico de listados de productos, resultados de queries cacheados o sesiones con muchos campos repetidos. También cuando los datos vienen de un almacén externo y quieres verificación criptográfica sin añadir una capa de middleware encima.

No es la herramienta adecuada si necesitas decodificación incremental en stream (el diccionario frontal lo impide por diseño), si tus datos son rowsets muy heterogéneos con pocas cadenas repetidas, o si ya usas JSON y te va bien: en ese caso el overhead de añadir una extensión nativa raramente compensa.

El repositorio oficial está en github.com/iliaal/phpser e incluye la especificación del formato de cable y el harness de benchmarking para que puedas medirlo con tus propios datos antes de adoptarlo.

Imagen: Pexels / Pixabay

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP