Pregunta C ++: dobles, precisión, máquinas virtuales y GCC


Tengo el siguiente fragmento de código:

#include <cstdio>
int main()
{
   if ((1.0 + 0.1) != (1.0 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

Cuando se compila con O3 usando gcc (4.4.4.5 y 4.6) y se ejecuta de forma nativa (ubuntu 10.10), imprime el resultado esperado de "igual".

Sin embargo, el mismo código cuando se compila como se describe arriba y se ejecuta en una máquina virtual (ubuntu 10.10, imagen de la caja virtual), emite "no igual"; este es el caso cuando las banderas O3 y O2 se configuran sin embargo O1 y abajo. Cuando se compila con clang (O3 y O2) y se ejecuta en la máquina virtual obtengo el resultado correcto.

Entiendo que 1.1 no se puede representar correctamente con el doble y que he leído "Lo que todo informático debería saber sobre la aritmética de coma flotante" así que por favor no me señale allí, esto parece ser algún tipo de optimización que hace GCC que de alguna manera no parece funcionar en máquinas virtuales.

¿Algunas ideas?

Nota: El estándar de C ++ dice que la promoción de tipo en estas situaciones depende de la implementación, ¿podría ser que GCC esté usando una representación interna más precisa que cuando se aplica la prueba de desigualdad se mantiene verdadera, debido a la precisión adicional?

ACTUALIZACIÓN1: La siguiente modificación de la pieza de código anterior, ahora da como resultado el resultado correcto. Parece que en algún momento, por alguna razón, GCC desactiva la palabra de control de coma flotante.

#include <cstdio>
void set_dpfpu() { unsigned int mode = 0x27F; asm ("fldcw %0" : : "m" (*&mode)); 
int main()
{
   set_dpfpu();
   if ((1.0 + 0.1) != (1.0 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

ACTUALIZACIÓN2: Para aquellos que preguntan sobre la naturaleza de la expresión const del código, lo he cambiado de la siguiente manera, y aún falla cuando se compila con GCC. - pero supongo que el optimizador puede convertir lo siguiente en una expresión const también.

#include <cstdio>
void set_dpfpu() { unsigned int mode = 0x27F; asm ("fldcw %0" : : "m" (*&mode)); 
int main()
{
   //set_dpfpu();  uncomment to make it work.
   double d1 = 1.0;
   double d2 = 1.0;  
   if ((d1 + 0.1) != (d2 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

ACTUALIZACIÓN3 Resolución: La actualización de virtualbox a la versión 4.1.8r75467 resolvió el problema. Sin embargo, su sigue siendo un problema, es decir: ¿por qué estaba funcionando la generación clang?


21
2018-01-18 19:31


origen


Respuestas:


ACTUALIZACIÓN: Ver esta publicación ¿Cómo lidiar con una precisión excesiva en cálculos de coma flotante? Aborda los problemas de la precisión del punto flotante extendido. Me olvidé de la precisión extendida en x86. Recuerdo una simulación que debería haber sido determinista, pero dio resultados diferentes en las CPU Intel que en las CPU PowePC. Las causas fueron la arquitectura de precisión extendida de Intel.

Esta página web habla sobre cómo lanzar CPU Intel en el modo de redondeo de doble precisión: http://www.network-theory.co.uk/docs/gccintro/gccintro_70.html.


¿Virtualbox garantiza que sus operaciones de coma flotante son idénticas a las operaciones de punto flotante del hardware? No pude encontrar una garantía como esa con una búsqueda rápida en Google. Tampoco encontré la promesa de que las operaciones de vituralbox FP se ajustan a IEEE 754.

Las VM son emuladores que intentan -y en su mayoría tienen éxito- emular un conjunto de instrucciones o una arquitectura en particular. Sin embargo, son solo emuladores y están sujetos a sus propias peculiaridades de implementación o problemas de diseño.

Si aún no lo hizo, publique la pregunta forums.virtualbox.org y vea lo que la comunidad dice al respecto.


10
2018-01-18 20:13



Sí, ese es un comportamiento realmente extraño, pero en realidad se puede explicar fácilmente:

En los registros de coma flotante x86 internamente, se usa más precisión (por ejemplo, 80 en lugar de 64). Esto significa que el cálculo 1.0 + 0.1 se computará con más precisión (y ya que 1.1 no se puede representar exactamente en binario en todos esos bits adicionales se usarán) en los registros. Solo cuando se almacena el resultado en la memoria se truncará.

Lo que esto significa es simple: si compara un valor cargado desde la memoria con un valor recién computado en los registros, obtendrá un retorno "no igual", porque un valor se truncó mientras que el otro no. Entonces, eso no tiene nada que ver con VM / sin VM, solo depende del código que genera el compilador, el cual puede fluctuar fácilmente tal como lo vemos allí.

Añádelo a la creciente lista de sorpresas en coma flotante.


5
2018-01-18 20:55



Puedo confirmar el mismo comportamiento de su código que no es de VM, pero como no tengo una VM no he probado la parte de VM.

Sin embargo, el compilador, tanto Clang como GCC evaluarán la expresión constante en tiempo de compilación. Ver el resultado del ensamblaje a continuación (usando gcc -O0 test.cpp -S)

    .file   "test.cpp"
    .section        .rodata
.LC0:
    .string "equal"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
        .section        .note.GNU-stack,"",@progbits

Parece que comprende el ensamblaje, pero está claro que solo existe la cadena "igual", no hay "igual". Entonces la comparación ni siquiera se hace en tiempo de ejecución, solo imprime "igual".

Intentaría codificar el cálculo y la comparación mediante el ensamblaje y ver si tiene el mismo comportamiento. Si tiene un comportamiento diferente en la VM, entonces es la forma en que la VM hace el cálculo.

ACTUALIZACIÓN 1: (Basado en la "ACTUALIZACIÓN 2" en la pregunta original). abajo esta el gcc -O0 -S test.cpp conjunto de salida (para arquitectura de 64 bits). En él puedes ver el movabsq $4607182418800017408, %rax línea dos veces. Esto será para las dos banderas de comparación, no he verificado, pero supongo que el valor de $ 4607182418800017408 es 1.1 en términos de coma flotante. Sería interesante compilar esto en la máquina virtual, si obtienes el mismo resultado (dos líneas similares), entonces la máquina virtual hará algo divertido en el tiempo de ejecución, de lo contrario, es una combinación de máquina virtual y compilador.

main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movabsq $4607182418800017408, %rax
        movq    %rax, -16(%rbp)
        movabsq $4607182418800017408, %rax
        movq    %rax, -8(%rbp)
        movsd   -16(%rbp), %xmm1
        movsd   .LC1(%rip), %xmm0
        addsd   %xmm1, %xmm0
        movsd   -8(%rbp), %xmm2
        movsd   .LC1(%rip), %xmm1
            addsd   %xmm2, %xmm1
        ucomisd %xmm1, %xmm0
        jp      .L6
        ucomisd %xmm1, %xmm0
        je      .L7

4
2018-01-18 20:38



Veo que agregaste otra pregunta:

Nota: El estándar de C ++ dice que la promoción de tipo en estas situaciones depende de la implementación, ¿podría ser que GCC esté usando una representación interna más precisa que cuando se aplica la prueba de desigualdad se mantiene verdadera, debido a la precisión adicional?

La respuesta a eso es no. 1.1 no es exactamente representable en un formato binario, sin importar cuántos bits tenga el formato. Puede acercarse, pero no con un número infinito de ceros después de la .1.

¿O te refieres a un formato interno completamente nuevo para decimales? No, me niego a creer eso. No sería muy compatible si lo hiciera.


2
2018-01-18 20:26