Yandex Open-Sources YaFF: un formato de cable de copia cero para Protobuf con velocidad de lectura cercana a la estructura

TLDR

YaFF es el formato de cable de copia cero de código abierto de Yandex para Protobuf: Apache 2.0, actualmente C++, v0.1.0. El archivo .proto sigue siendo la fuente de la verdad; sólo cambia el diseño de la memoria física. En los puntos de referencia de Yandex, Flat Layout lee datos activos ~3,8 veces más rápido que FlatBuffers, dentro de 1,2 veces de una estructura C++ sin formato. Cuatro diseños (fijo, plano, disperso y dinámico) intercambian velocidad de lectura por flexibilidad de esquema; Dinámico es el valor predeterminado. YaFF se ejecuta en su sistema de recomendación publicitaria, donde informa un ahorro de CPU del 10 al 20 % a escala de producción. La adopción es incremental: colóquelo en una ruta activa, con conversión Protobuf bidireccional en los bordes.

Yandex tiene YaFF (otro formato plano más) de código abierto en Apache 2.0. Es una biblioteca de serialización C++ de alto rendimiento. YaFF proporciona un formato de cable de copia cero para el ecosistema Protobuf. Su archivo .proto sigue siendo la única fuente de verdad. El formato sólo cambia la forma en que los datos se almacenan en la memoria. Se concentra en tiempos de ejecución del lado del servidor.

¿Qué es YaFF?

YaFF no reemplaza a Protobuf. Es un formato de cable alternativo para mensajes Protobuf. El mismo esquema .proto genera una API C++ similar a un prototipo. Las lecturas no necesitan ningún paso de análisis, por lo que los campos provienen directamente del búfer. El código menos sensible al rendimiento aún puede analizar el formato de conexión nuevamente en mensajes Protobuf. Esa conversión bidireccional es lo que hace que la adopción módulo por módulo sea realista. Introduces YaFF en una ruta activa y dejas el resto en Protobuf.

El problema al que se dirige

El análisis de Protobuf puede consumir porcentajes de CPU de dos dígitos en backends de alta carga. A escala, eso se asigna a miles de núcleos físicos. La opción común de copia cero es FlatBuffers, también de Google. Pero FlatBuffers no es un complemento de Protobuf y requiere mantener un esquema y una capa de conversión separados. semánticamente incompatible con Protobuf. Migrar significa esquemas duplicados, diferentes reglas de evolución de esquemas y convertidores de campos escritos a mano. Muchos equipos concluyen que el coste no merece la pena. YaFF apunta a esa brecha: lecturas de copia cero con la semántica de Protobuf preservada.

Cómo funcionan los diseños

Un diseño decide cómo se almacena un mensaje en el búfer. Cambia solo la representación física, dejando el esquema y las interfaces generadas sin cambios. YaFF envía cuatro diseños. Lo fijo es una estructura empaquetada sin encabezado y con un esquema congelado. Flat agrega un encabezado de dos bytes y admite la evolución del esquema. Campos de direcciones dispersas a través de una metatabla, que se ajustan a esquemas dispersos. Dinámico es el valor predeterminado y selecciona Plano o Escaso en tiempo de ejecución. Utiliza Flat mientras el esquema lo permite, luego cambia a Sparse cuando la evolución rompe la alineación plana.

DiseñoAcceso de lecturaSobrecarga por mensajeEvolución del esquemaMejor paraFijo1 lectura, 0 ramas0 bytesCongeladoPequeñas primitivas en líneaPlano2 lecturas, 1 rama2 bytesRestringido (conservación de tipo)Datos densos y activosEscasos4 lecturas, 2 ramas6 bytesSin restriccionesEsquemas dispersos, evolución libreDinámico (predeterminado)Plano o disperso en tiempo de ejecución2 o 6 bytesSin restriccionesGeneral lógica de aplicación

Punto de referencia

Yandex incluye un conjunto de pruebas comparativas reproducibles, creado con google/benchmark en una versión de lanzamiento. Los números siguientes son nanosegundos medios por lectura en un AMD EPYC 7713 con Clang 20.1.8. Más abajo es más rápido. En el caso jerárquico más candente, el diseño plano se lee en 9,79 ns. FlatBuffers necesita 37,30 ns y Protobuf necesita 219,35 ns. La línea base de la estructura C++ sin formato es 8,14 ns. Entonces, Flat Layout lee aproximadamente 3,8 veces más rápido que FlatBuffers aquí, y aproximadamente 22 veces más rápido que Protobuf. Se mantiene dentro de 1,2 × de la estructura sin procesar.

FormatoTiempo de lectura (ns)Desaceleración frente a estructura sin formatoEstructura C++ sin procesar8.141.0×Diseño plano de YaFF9.791.2×Diseño disperso de YaFF21.232.6×FlatBuffers37.304.6×Protobuf219.3526.9×
Mediana de ns por lectura, almacenamiento en caché jerárquico/caliente/sin cadena. Fuente: https://yaff.tech/docs/en/benchmarks/access

Nota: Los números absolutos dependen de la CPU y la memoria del host. Se espera que las proporciones entre formatos se mantengan en todo el hardware.

El detalle del alias del compilador

FlatBuffers y YaFF leen campos reinterpretando la memoria sin procesar como el tipo de destino. Ese juego de palabras deja a TBAA sin datos suficientemente sólidos. Por lo tanto, el análisis de alias de LLVM recae en un veredicto conservador de MayAlias. Entonces, el compilador no puede probar que los accesos repetidos sean seguros para reutilizarse. Escribir root.intermediate().leaf().a() dos veces vuelve a recorrer el árbol cada vez. YaFF agrega anotaciones en su código generado que le indican al compilador cuándo es segura la reutilización. Las anotaciones del código generado de YaFF a menudo pueden ayudar al compilador a reutilizar la cadena de acceso, siempre que la memoria relevante no se modifique entre lecturas. Mientras no se escriba nada en la memoria entre lecturas, YaFF almacena en caché la cadena de acceso por sí solo.

Dónde encaja: casos de uso

YaFF apunta a sistemas en los que usted controla tanto al productor como al consumidor. Los backends de recomendación y publicación de anuncios son los más adecuados. Según Yandex, YaFF se ejecuta en su sistema de recomendación de publicidad, donde informa un ahorro de CPU del 10 al 20 % a escala de producción. Los índices mapeados en memoria son una segunda opción. Un host puede contener decenas de gigabytes de datos locales. Esos índices compatibles con mmap sobreviven a los reinicios del servicio sin volver a analizarlos. Los índices de búsqueda, las tiendas de funciones y los servicios de feeds comparten ese perfil de lectura intensa. El diseño de columnas planificado está dirigido a análisis y canalizaciones de aprendizaje automático con grandes campos repetidos. YaFF también puede ser más compacto que FlatBuffers, lo que ayuda al comportamiento de la caché.

Una mirada al código

La ruta de lectura refleja Protobuf, menos el paso de análisis.

#include “feed.pb.h” // generado por protocolo #include “feed.yaff.h” // generado por yaff_generate() // 1. Serializar un mensaje Protobuf existente en un búfer YaFF. feed::FeedResponse proto = LoadFeedResponse(); const auto buffer = yaff::Serialize(proto); // 2. Leer campos directamente desde el búfer. No hay ningún paso de análisis. const auto& respuesta = yaff::ReadMessage(buffer.Data()); for (const auto& item: respuesta.items()) { std::string_view title = item.title(); std::string_view autor = item.autor().nombre(); // vacío si el autor no está configurado } // 3. Volver a convertir a Protobuf cuando un consumidor necesite el mensaje analizado. feed::FeedResponse restaurado; respuesta.ParseTo(restaurado);

Agrega YaFF a través de CMake (find_package) o Conan. La generación de código ejecuta protobuf_generate() y luego yaff_generate(). Los tipos de YaFF generados viven en el espacio de nombres protoyaff::. La mayoría de los proyectos solo vinculan yaff::core y yaff::proto.

Recursos:

Consulte el repositorio y la documentación de GitHub.