Pregunta

En un programa de código abierto I escribió , estoy leyendo datos binarios (escritos por otro programa) de un archivo y generando ints, dobles, y otros tipos de datos variados. Uno de los desafíos es que necesita ejecutarse en máquinas de 32 bits y 64 bits de ambas características, lo que significa que yo terminar teniendo que hacer un poco de bajo nivel de giro de bits. Conozco un (muy) un poco sobre el tipo de juego de palabras y el alias estricto y quiero asegurarme de que estoy haciendo las cosas de la manera correcta.

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 conjunto de funciones de soporte para intercambiar órdenes de bytes según sea necesario, como como:

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 resistencia 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 estoy tratando de lanzar un char * a un doble. Carné de identidad quisiera reutilizar el código de intercambio 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 "int64todouble.i" asignación y romper el programa ¿Hay una manera más segura de hacer esto, mientras se considera que este programa debe mantenerse optimizado para el rendimiento, y también que prefiero no escribir un conjunto paralelo de transformaciones para emitir char * a doble directamente? Si el método sindical de castigo es seguro, ¿debería ser reescribir mis funciones como snativeint64_t para usarlo?


Terminé usando la respuesta Steve Jessop porque las funciones de conversión se reescribieron 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 de 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!

¿Fue útil?

Solución

Dado que parece saber lo suficiente acerca de 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 peligro una memoria. Entonces ni siquiera tiene que pensar en el alias.

Dado que está utilizando un puntero de función para una función que podría alinearse fácilmente si estuviera dispuesto a lanzar varios archivos binarios, el rendimiento no debe ser un gran problema de todos modos, pero le gustaría saber que algunos compiladores pueden ser bastante diabólicos. Optimización de la memoria: para tamaños enteros pequeños, se puede insertar un conjunto de cargas y almacenes, e incluso puede encontrar que las variables se optimizan por completo y el compilador realiza la "copia". simplemente reasigne las ranuras de pila que está usando para las variables, como 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ílelo. Lo más probable es que, en el peor de los casos, no será lento.

En general, sin embargo, hacer algo demasiado inteligente con el intercambio de bytes resulta en problemas de portabilidad. Existen ABI con dobles endian medios, donde cada palabra es little endian, pero la palabra grande 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 solo está duplicando IEEE se duplica desde un archivo de entrada en un formato a un archivo de salida en otro formato (no estoy seguro de si lo es, ya que no conozco los formatos de la base de datos en cuestión, pero si es así), entonces quizás puede olvidarse del hecho de que es un doble, ya que de todos modos no lo está utilizando para la aritmética. Simplemente trátelo como un carácter opaco [8], que requiere el intercambio de bytes solo si los formatos de archivo difieren.

Otros consejos

Le recomiendo que lea Comprensión del alias estricto . Específicamente, vea las secciones etiquetadas "Fundición a través de una unión". Tiene varios ejemplos muy buenos. Si bien el artículo está en un sitio web sobre el procesador Cell y usa ejemplos de ensamblaje PPC, casi todo es igualmente aplicable a otras arquitecturas, incluido x86.

El estándar dice que escribir en un campo de un sindicato y leerlo de inmediato es un comportamiento indefinido. Entonces, si sigue 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 similar a una plantilla en C usando un conjunto de macros usando los tipos de entrada y salida como parámetros.

Como una sugerencia muy pequeña, le sugiero que investigue si puede intercambiar el enmascaramiento y el desplazamiento, en el caso de 64 bits. Dado que la operación está intercambiando bytes, siempre debería poder salirse 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 descifrarlo.

En resumen, cambiar esto:

(((wrongend & 0xff00000000000000LL) >> 56)

en esto:

((wrongend >> 56) & 0xff)

debería generar el mismo resultado.

Editar:
Se eliminaron los comentarios sobre cómo almacenar datos de manera efectiva siempre big endian e intercambiar a endianess de máquina, ya que el interlocutor no ha mencionado que otro programa escribe sus datos (que es información importante). Aún así, si los datos necesitan conversión de cualquier endian a grande y de grande a host endian, ntohs / ntohl / htons / htonl son los mejores métodos, los más elegantes e inmejorables en velocidad (ya que realizarán tareas en hardware si la CPU lo admite, no se puede superar eso).


Con respecto a double / float, solo almacénelos en ints mediante la conversió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élvelo en una función

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

double int64ToDouble(int64_t i)
{
    return *(double *)&i;
}

El interlocutor proporcionó este enlace:

http: // cocoawithlove .com / 2008/04 / using-pointers-to-recast-in-c-is-bad.html

como prueba de que el casting es malo ... desafortunadamente solo puedo estar muy en desacuerdo con la mayor parte de esta página. Citas y comentarios:

  

Tan común como lanzar a través de un puntero   es que en realidad es una mala práctica y   código potencialmente arriesgado. Fundición   a través de un puntero tiene el potencial de   crear errores debido al tipo de castigo.

No es nada arriesgado 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 lenguaje. Según ese argumento, debe dejar de programar por completo.

  

Tipo de punteo
Una forma de puntero   aliasing donde dos punteros y referir   a la misma ubicación en la memoria pero   representar esa ubicación como diferente   tipos. El compilador tratará ambos   "juegos de palabras" como punteros no relacionados. Tipo   castigar tiene el potencial de causar   problemas de dependencia para cualquier dato   Se accede a través de ambos punteros.

Esto es cierto, pero desafortunadamente no tiene ninguna relación con 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 el mismo tipo. Esta es la situación que debes resolver con un sindicato, cualquier otra cosa es bastante mala. ¡Mal, 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) y inmediatamente lo deferencia . Una vez que las funciones regresan, no hay ningún puntero sujeto a nada. Hay un int64 y un doble y estos no tienen ninguna relación 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 muestra 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 punning 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, solo escrito 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 incluso 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 se define para ser siempre de 64 bits (a diferencia de int no varía en tamaño, está fijado en 64 bits), por lo tanto, siempre encajará en una variable de tamaño int64_t.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top