todas - Portabilidad de la serialización binaria de tipo doble/flotante en C++
tipos de parametros en c++ (8)
El estándar de C ++ no discute el diseño subyacente de los tipos float y double, solo el rango de valores que deberían representar. (Esto también es cierto para los tipos con signo, es un cumplido de dos o algo más)
Mi pregunta es: ¿Cuáles son las técnicas utilizadas para serializar / deserializar los tipos de POD, como el doble y el flotador de manera portátil? Por el momento parece que la única forma de hacerlo es tener el valor representado literalmente (como en "123.456"), el diseño ieee754 para el doble no es estándar en todas las arquitecturas.
¿Qué pasa con un formato legible para humanos?
Tiene un par de ventajas sobre el binario:
- Es legible
- Es portátil
- Hace que el soporte sea realmente fácil
(como puede pedirle al usuario que lo mire en su editor favorito, incluso la palabra) - Es fácil de arreglar
(o ajuste archivos manualmente en situaciones de error)
Desventaja:
- No es compacto
Si esto es un problema real, siempre puedes comprimirlo. - Puede ser un poco más lento para extraer / generar
Tenga en cuenta que un formato binario probablemente también deba ser normalizado (veahtonl()
)
Para producir un doble con precisión completa:
double v = 2.20;
std::cout << std::setprecision(std::numeric_limits<double>::digits) << v;
DE ACUERDO. No estoy convencido de que sea exactamente preciso. Puede perder precisión.
Cree una interfaz de serializador / de-serializador apropiada para escribir / leer esto.
La interfaz puede tener varias implementaciones y puede probar sus opciones.
Como dije antes, las opciones obvias serían:
- IEEE754 que escribe / lee el fragmento binario si es soportado directamente por la arquitectura o lo analiza si no es compatible con la arquitectura
- Texto: siempre debe analizarse.
- Cualquier cosa que puedas pensar.
Solo recuerda: una vez que tienes esta capa, siempre puedes comenzar con IEEE754 si solo ayudas a las plataformas que usan este formato internamente. ¡De esta manera tendrá el esfuerzo adicional solo cuando necesite soportar una plataforma diferente! No hagas trabajo, no es necesario.
Creo que la respuesta "depende" de lo que sea su aplicación particular y su perfil de rendimiento.
Digamos que tiene un entorno de datos de mercado de baja latencia, y luego usar cadenas es francamente tonto. Si la información que está transmitiendo es de precios, entonces los dobles (y la representación binaria de ellos) realmente son difíciles de manejar. Mientras que, si realmente no le importa el rendimiento, y lo que quiere es visibilidad (almacenamiento, transmisión), entonces las cadenas son un candidato ideal.
De hecho, optaría por una representación mantissa / exponente integral de flotantes / dobles - es decir, en la primera oportunidad, convierta el float / double a un par de enteros y luego transmítalo. Entonces solo debe preocuparse por la portabilidad de los enteros y las diversas rutinas (como las rutinas hton()
para manejar las conversiones por usted). También almacene todo en la endianess de su plataforma más frecuente (por ejemplo, si solo está usando linux, entonces, ¿qué sentido tiene almacenar cosas en big endian?)
Debe convertirlos a un formato que siempre podrá usar para recrear sus flotantes / dobles.
Esto podría usar una representación de cadena o, si necesita algo que ocupa menos espacio, representar su número en ieee754 (o cualquier otro formato que elija) y luego analizarlo como lo haría con una cadena.
Eche un vistazo a la (antigua) implementación del archivo gtypes.h en glib 2 - incluye lo siguiente:
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
union _GFloatIEEE754
{
gfloat v_float;
struct {
guint mantissa : 23;
guint biased_exponent : 8;
guint sign : 1;
} mpn;
};
union _GDoubleIEEE754
{
gdouble v_double;
struct {
guint mantissa_low : 32;
guint mantissa_high : 20;
guint biased_exponent : 11;
guint sign : 1;
} mpn;
};
#elif G_BYTE_ORDER == G_BIG_ENDIAN
union _GFloatIEEE754
{
gfloat v_float;
struct {
guint sign : 1;
guint biased_exponent : 8;
guint mantissa : 23;
} mpn;
};
union _GDoubleIEEE754
{
gdouble v_double;
struct {
guint sign : 1;
guint biased_exponent : 11;
guint mantissa_high : 20;
guint mantissa_low : 32;
} mpn;
};
#else /* !G_LITTLE_ENDIAN && !G_BIG_ENDIAN */
#error unknown ENDIAN type
#endif /* !G_LITTLE_ENDIAN && !G_BIG_ENDIAN */
El SQLite4 usa un nuevo formato para almacenar dobles y flotadores
- Funciona de manera confiable y consistente incluso en plataformas que carecen de soporte para números de punto flotante binary64 IEEE 754.
- Los cálculos de moneda normalmente se pueden hacer exactamente y sin redondeo.
- Cualquier entero de 64 bits con signo o sin signo se puede representar exactamente.
- El rango y la precisión del punto flotante superan los números de punto flotante binary64 de IEEE 754.
- El infinito positivo y negativo y NaN (Not-a-Number) tienen representaciones bien definidas.
Fuentes:
Simplemente escriba la representación binaria IEEE754 en el disco y documente esto como su formato de almacenamiento (junto con endianness). Luego, depende de la implementación convertir esto a su representación interna si es necesario.
Brian "Beej Jorgensen" Hall le da a su Guía de Programación de Red un código para empaquetar float
(o double
) a uint32_t
(resp uint64_t
) para poder transmitirlo de manera segura a través de la red entre dos máquinas que pueden no estar de acuerdo con su representación. Tiene alguna limitación, principalmente no admite NaN e infinito.
Aquí está su función de embalaje:
#define pack754_32(f) (pack754((f), 32, 8))
#define pack754_64(f) (pack754((f), 64, 11))
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
long double fnorm;
int shift;
long long sign, exp, significand;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (f == 0.0) return 0; // get this special case out of the way
// check sign and begin normalization
if (f < 0) { sign = 1; fnorm = -f; }
else { sign = 0; fnorm = f; }
// get the normalized form of f and track the exponent
shift = 0;
while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
fnorm = fnorm - 1.0;
// calculate the binary form (non-float) of the significand data
significand = fnorm * ((1LL<<significandbits) + 0.5f);
// get the biased exponent
exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
// return the final answer
return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}