Apuntar con seguridad a char*para doblar en C
endianness type-punning (5)
Como parece que sabe lo suficiente sobre su implementación para asegurarse de que int64_t y double son del mismo tamaño y tienen representaciones de almacenamiento adecuadas, puede poner en riesgo una memcpy. Entonces ni siquiera tienes que pensar en aliasing.
Dado que está utilizando un puntero a función para una función que podría incluirse fácilmente si estuviera dispuesto a liberar varios binarios, el rendimiento no debe ser un gran problema, pero puede que quiera saber que algunos compiladores pueden ser bastante diabólicos optimizando memcpy. para tamaños enteros pequeños, un conjunto de cargas y tiendas puede estar incluido, e incluso puede encontrar que las variables se optimizan por completo y el compilador hace la "copia" simplemente reasignar las ranuras de pila que está utilizando para las variables, al igual que una unión.
int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you''re lucky */
printf("%lf", d);
Examine el código resultante, o simplemente perfórelo. Las posibilidades son incluso en el peor de los casos, no será lento.
En general, sin embargo, hacer algo demasiado inteligente con byteswapping resulta en problemas de portabilidad. Existen ABIs con dobles del middle-endian, donde cada palabra es little-endian, pero la gran palabra es lo primero.
Normalmente podría considerar almacenar sus dobles usando sprintf y sscanf, pero para su proyecto los formatos de archivo no están bajo su control. Pero si su aplicación simplemente está descargando IEEE, se duplica desde un archivo de entrada en un formato a un archivo de salida en otro formato (no estoy seguro si lo es, ya que no conozco los formatos de la base de datos en cuestión, pero si es así), entonces tal vez puede olvidarse del hecho de que es un doble, ya que no lo está usando para aritmética de todos modos. Simplemente trátelo como un carácter opaco [8], que requiere el uso de bytes solo si los formatos de archivo son diferentes.
En un programa de código abierto que escribí , estoy leyendo datos binarios (escritos por otro programa) desde un archivo y generando entradas, dobles y otros tipos de datos clasificados. Uno de los desafíos es que tiene que ejecutarse en máquinas de 32 bits y de 64 bits de ambos endiannesses, lo que significa que termino teniendo que hacer un poco de bit-twiddling de bajo nivel. Conozco (muy) poco sobre el tipo de juego de palabras y el alias estricto y quiero asegurarme de que estoy haciendo las cosas bien.
Básicamente, es fácil convertir de un char * a un int de varios tamaños:
int64_t snativeint64_t(const char *buf)
{
/* Interpret the first 8 bytes of buf as a 64-bit int */
return *(int64_t *) buf;
}
y tengo un elenco de funciones de soporte para intercambiar órdenes de bytes según sea necesario, como por ejemplo:
int64_t swappedint64_t(const int64_t wrongend)
{
/* Change the endianness of a 64-bit integer */
return (((wrongend & 0xff00000000000000LL) >> 56) |
((wrongend & 0x00ff000000000000LL) >> 40) |
((wrongend & 0x0000ff0000000000LL) >> 24) |
((wrongend & 0x000000ff00000000LL) >> 8) |
((wrongend & 0x00000000ff000000LL) << 8) |
((wrongend & 0x0000000000ff0000LL) << 24) |
((wrongend & 0x000000000000ff00LL) << 40) |
((wrongend & 0x00000000000000ffLL) << 56));
}
En tiempo de ejecución, el programa detecta la endianidad de la máquina y asigna uno de los anteriores a un puntero de función:
int64_t (*slittleint64_t)(const char *);
if(littleendian) {
slittleint64_t = snativeint64_t;
} else {
slittleint64_t = sswappedint64_t;
}
Ahora, la parte difícil viene cuando intento lanzar un char * a un doble. Me gustaría volver a usar el código de intercambio de endian así:
union
{
double d;
int64_t i;
} int64todouble;
int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);
Sin embargo, algunos compiladores podrían optimizar la asignación "int64todouble.i" y romper el programa. ¿Hay alguna manera más segura de hacerlo, teniendo en cuenta que este programa debe mantenerse optimizado para el rendimiento, y también que prefiero no escribir un conjunto paralelo de transformaciones para convertir char * al doble directamente? Si el método de unión sindical es seguro, ¿debería volver a escribir mis funciones como snativeint64_t para usarlo?
Terminé usando la respuesta de Steve Jessop porque las funciones de conversión se volvieron a escribir para usar memcpy, así:
int64_t snativeint64_t(const char *buf)
{
/* Interpret the first 8 bytes of buf as a 64-bit int */
int64_t output;
memcpy(&output, buf, 8);
return output;
}
compilado en el mismo ensamblador exacto que mi código original:
snativeint64_t:
movq (%rdi), %rax
ret
De los dos, la versión memcpy expresa más explícitamente lo que estoy tratando de hacer y debería funcionar incluso en los compiladores más ingenuos.
Adam, tu respuesta también fue maravillosa y aprendí mucho de ella. ¡Gracias por publicar!
Como sugerencia muy pequeña, sugiero que investigue si puede cambiar el enmascaramiento y el cambio en el caso de 64 bits. Dado que la operación está intercambiando bytes, siempre debería poder salir con una máscara de solo 0xff
. Esto debería conducir a un código más rápido y compacto, a menos que el compilador sea lo suficientemente inteligente como para descubrirlo por sí mismo.
En resumen, cambiando esto:
(((wrongend & 0xff00000000000000LL) >> 56)
dentro de esto:
((wrongend >> 56) & 0xff)
debería generar el mismo resultado
El estándar dice que escribir en un campo de un sindicato y leer de él inmediatamente es un comportamiento indefinido. Por lo tanto, si utiliza el libro de reglas, el método basado en la unión no funcionará.
Las macros suelen ser una mala idea, pero esto podría ser una excepción a la regla. Debería ser posible obtener un comportamiento tipo plantilla en C usando un conjunto de macros usando los tipos de entrada y salida como parámetros.
Editar:
Se eliminaron los comentarios sobre cómo almacenar datos de manera efectiva siempre con Big Endian e intercambiar a Endianess de la máquina, ya que el interlocutor no ha mencionado otro programa escribe sus datos (que es información importante).
Aún así, si los datos necesitan conversión de endian a big y de big a host endian, ntohs / ntohl / htons / htonl son los mejores métodos, más elegantes e imbatibles en velocidad (ya que realizarán tareas en hardware si la CPU lo admite, no se puede superar eso).
En cuanto a double / float, simplemente guárdalos en ints por fundición de memoria:
double d = 3.1234;
printf("Double %f/n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f/n", d2);
Envuélvalo en una función
int64_t doubleToInt64(double d)
{
return *(int64_t *)&d;
}
double int64ToDouble(int64_t i)
{
return *(double *)&i;
}
Interlocutor proporcionó este enlace:
http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html
como una prueba de que el casting es malo ... desafortunadamente solo puedo estar totalmente en desacuerdo con la mayoría de esta página. Citas y comentarios:
Tan común como lanzar un puntero es, en realidad, es una mala práctica y un código potencialmente arriesgado. Lanzar a través de un puntero tiene el potencial de crear errores debido al tipo de juego de palabras.
No es arriesgado en absoluto y tampoco es una mala práctica. Solo tiene el potencial de causar errores si lo hace incorrectamente, al igual que la programación en C tiene el potencial de causar errores si lo hace incorrectamente, al igual que cualquier programación en cualquier idioma. Con ese argumento, debes dejar de programar por completo.
Tipo de juego de palabras
Una forma de alias de puntero donde dos punteros se refieren a la misma ubicación en la memoria pero representan esa ubicación como tipos diferentes. El compilador tratará ambos "juegos de palabras" como punteros no relacionados. Los juegos de palabras tipo tienen el potencial de causar problemas de dependencia para los datos a los que se accede a través de ambos punteros.
Esto es cierto, pero lamentablemente totalmente ajeno a mi código .
A lo que se refiere es a un código como este:
int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;
Ahora doublePointer e intPointer apuntan a la misma ubicación de memoria, pero tratan esto como del mismo tipo. Esta es la situación que debes resolver con una unión, cualquier otra cosa es bastante mala. ¡Malo, eso no es lo que hace mi código!
Mi código se copia por valor , no por referencia . Lanzo un puntero doble a int64 (o al revés) e inmediatamente lo deferencia . Una vez que las funciones regresan, no hay ningún puntero retenido por nada. Hay un int64 y un doble y estos no están relacionados con el parámetro de entrada de las funciones. Nunca copio ningún puntero a un puntero de un tipo diferente (si vio esto en mi ejemplo de código, leyó mal el código C que escribí), simplemente transfiero el valor a una variable de tipo diferente (en una ubicación de memoria propia) . Por lo tanto, la definición de tipo de juego de palabras no se aplica en absoluto, ya que dice "se refieren a la misma ubicación en la memoria" y nada aquí se refiere a la misma ubicación de memoria.
int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;
Mi código no es más que una copia de memoria, simplemente escrita en C sin una función externa.
int64_t doubleToInt64(double d)
{
return *(int64_t *)&d;
}
Podría escribirse como
int64_t doubleToInt64(double d)
{
int64_t result;
memcpy(&result, &d, sizeof(d));
return result;
}
No es nada más que eso, por lo que no hay ningún tipo de juego de palabras siquiera a la vista en ningún lado. Y esta operación también es totalmente segura, tan segura como una operación puede ser en C. Un doble está definido para ser siempre de 64 bits (a diferencia de int no varía en tamaño, se fija en 64 bits), por lo tanto, siempre cabe en una variable de tamaño int64_t.
Le sugiero que lea Understanding String Aliasing . Específicamente, vea las secciones etiquetadas "Fundir a través de una unión". Tiene una serie de muy buenos ejemplos. Si bien el artículo está en un sitio web sobre el procesador Cell y utiliza ejemplos de ensamblaje de PPC, casi todo es igualmente aplicable a otras arquitecturas, incluida x86.