c++ - ejemplo - serializacion y deserializacion en java
¿Los cereales y la serialización de Boost usan cero copias? (2)
He hecho una comparación de rendimiento entre varios protocolos de serialización, incluyendo FlatBuffers, Cap''n Proto, serialización Boost y cereal. Todas las pruebas están escritas en C ++.
Sé que FlatBuffers y Cap''n Proto usan cero copias. Con copia cero, el tiempo de serialización es nulo, pero el tamaño de los objetos serializados es mayor.
Pensé que la serialización de cereales y Boost no usaba copia cero. Sin embargo, el tiempo de serialización (para int y double) es casi nulo, y el tamaño de los objetos serializados es casi el mismo que el de Cap''n Proto o Flatbuffers. No encontré ninguna información sobre copia cero en sus documentos.
¿La serialización de cereales y Boost también utiliza cero copias?
Boost Serialization es extensible.
Permite que sus tipos describan lo que se debe serializar, y los archivos para describir el formato.
Esto puede ser "copia cero", es decir, el único almacenamiento en memoria intermedia está en la secuencia que recibe sus datos (por ejemplo, el descriptor de socket o archivo).
Para ver un ejemplo de implementación de serialización de dynamic_bitset conscientemente sin copia, vea el código en esta respuesta: ¿Cómo serializar boost :: dynamic_bitset?
Tengo un número de estos en el sitio. BOOST_IS_BITWISE_SERIALIZABLE
también la documentación de BOOST_IS_BITWISE_SERIALIZABLE
y el efecto que tiene en la serialización de contenedores (si serializa una colección asignada contiguamente de datos serializables en bits, el resultado es copia cero o incluso __memcpy_sse4
etc.).
Nota al margen: Cap''n proto hace algo completamente diferente, AFAIK: clasifica algunos objetos como futuros a los datos. Esto es aparentemente lo que anuncian agresivamente como "∞% más rápido, 0μs !!!" (que es algo cierto en el caso donde los datos nunca se recuperan).
Boost y Cereal no implementan copia cero en el sentido de Cap''n Proto o Flatbuffers.
Con una verdadera serialización de copia cero, el almacén de respaldo para sus objetos vivos en memoria es, de hecho, exactamente el mismo segmento de memoria que se pasa a las llamadas al sistema de read()
o write()
. No hay ningún paso de embalaje / desembalaje.
En general, esto tiene una serie de implicaciones:
- Los objetos no se asignan usando new / delete. Al construir un mensaje, primero se asigna el mensaje, que asigna un espacio de memoria contiguo largo para el contenido del mensaje. A continuación, asigna la estructura del mensaje directamente dentro del mensaje , recibiendo punteros que de hecho apuntan a la memoria del mensaje. Cuando se escribe el mensaje más tarde, una sola llamada de
write()
saca todo el espacio de la memoria al cable. - De manera similar, cuando lee un mensaje, una sola llamada de
read()
o tal vez 2-3 lee todo el mensaje en un bloque de memoria. A continuación, obtiene un puntero (o un objeto similar a un puntero) en la "raíz" del mensaje, que puede usar para recorrerlo. Tenga en cuenta que ninguna parte del mensaje se inspecciona en realidad hasta que la aplicación lo atraviesa. - Con los enchufes normales, las únicas copias de sus datos suceden en el espacio del kernel . Con la red RDMA, es posible que incluso pueda evitar las copias del espacio del núcleo: los datos salen del cable directamente a su ubicación de memoria final.
- Al trabajar con archivos (en lugar de redes), es posible
mmap()
un mensaje muy grande ammap()
directamente desde el disco y usar la región de memoria asignada directamente. Hacerlo es O (1) - no importa cuán grande sea el archivo. Su sistema operativo buscará automáticamente en las partes necesarias del archivo cuando realmente acceda a ellas. - Dos procesos en la misma máquina se pueden comunicar a través de segmentos de memoria compartida sin copias. Tenga en cuenta que, por lo general, los viejos objetos normales de C ++ no funcionan bien en la memoria compartida, porque los segmentos de memoria generalmente no tienen la misma dirección en ambos espacios de memoria, por lo tanto, todos los punteros son incorrectos. Con un marco de serialización de copia cero, los punteros generalmente se expresan como desplazamientos en lugar de direcciones absolutas, de modo que son independientes de la posición.
Boost y Cereal son diferentes: cuando recibe un mensaje en estos sistemas, primero se realiza un pase sobre el mensaje completo para "descomprimir" el contenido. El lugar de descanso final de los datos está en objetos asignados de la manera tradicional utilizando new / delete. De forma similar, cuando se envía un mensaje, los datos deben recopilarse de este árbol de objetos y agruparse en un búfer para poder escribirlos. A pesar de que Boost y Cereal son "extensibles", ser verdaderamente cero copia requiere un diseño subyacente muy diferente; no se puede atornillar como una extensión.
Dicho esto, no asuma que la copia cero siempre será más rápida. memcpy()
puede ser bastante rápido, y el resto de su programa puede reducir el costo. Mientras tanto, los sistemas de copia cero tienden a tener inconvenientes API, particularmente debido a las restricciones en la asignación de memoria. En general, puede usar mejor su tiempo para usar un sistema de serialización tradicional.
El lugar donde la copia cero es más obviamente ventajosa es cuando se manipulan los archivos, ya que como mencioné, puede fácilmente mmap()
un archivo enorme y solo leer parte de él. Los formatos sin copia cero simplemente no pueden hacer eso. Sin embargo, cuando se trata de redes, las ventajas son menos claras, ya que la comunicación de red en sí misma es necesariamente O (n).
Al final del día, si realmente quiere saber qué sistema de serialización es el más rápido para su caso de uso, probablemente deba probarlos todos y medirlos. Tenga en cuenta que los puntos de referencia de juguetes suelen ser engañosos; debe probar su caso de uso real (o algo muy similar) para obtener información útil.
Divulgación: soy el autor de Cap''n Proto (un serializador sin copia) y Protocol Buffers v2 (un popular serializador que no es de copia cero).