Pregunta

¿Las operaciones de punto flotante de C#/.NET difieren en precisión entre el modo de depuración y el modo de lanzamiento?

¿Fue útil?

Solución

De hecho, pueden ser diferentes.Según la especificación CLR ECMA:

Las ubicaciones de almacenamiento para números de punto flotante (estadísticas, elementos de matriz y campos de clases) son de tamaño fijo.Los tamaños de almacenamiento compatibles son Float32 y Float64.En cualquier otro lugar (en la pila de evaluación, como argumentos, como tipos de devolución y como variables locales) se representan números de punto flotante utilizando un tipo de punto flotante interno.En cada caso, el tipo nominal de la variable o expresión es R4 o R8, pero su valor puede representarse internamente con un rango y/o precisión adicionales.El tamaño de la representación interna del punto flotante depende de la implementación, puede variar y tendrá precisión al menos tan grande como la de la variable o expresión que se representa.Se realiza una conversión amplia implícita a la representación interna de Float32 o Float64 cuando esos tipos se cargan desde el almacenamiento.La representación interna es típicamente el tamaño nativo para el hardware, o según sea necesario para la implementación eficiente de una operación.

Lo que esto significa básicamente es que la siguiente comparación puede ser igual o no:

class Foo
{
  double _v = ...;

  void Bar()
  {
    double v = _v;

    if( v == _v )
    {
      // Code may or may not execute here.
      // _v is 64-bit.
      // v could be either 64-bit (debug) or 80-bit (release) or something else (future?).
    }
  }
}

Llevar el mensaje a casa:nunca verifique la igualdad de los valores flotantes.

Otros consejos

Esta es una pregunta interesante, así que experimenté un poco.Usé este código:

static void Main (string [] args)
{
  float
    a = float.MaxValue / 3.0f,
    b = a * a;

  if (a * a < b)
  {
    Console.WriteLine ("Less");
  }
  else
  {
    Console.WriteLine ("GreaterEqual");
  }
}

utilizando DevStudio 2005 y .Net 2.Compilé como depuración y lanzamiento y examiné el resultado del compilador:

Release                                                    Debug

    static void Main (string [] args)                        static void Main (string [] args)
    {                                                        {
                                                        00000000  push        ebp  
                                                        00000001  mov         ebp,esp 
                                                        00000003  push        edi  
                                                        00000004  push        esi  
                                                        00000005  push        ebx  
                                                        00000006  sub         esp,3Ch 
                                                        00000009  xor         eax,eax 
                                                        0000000b  mov         dword ptr [ebp-10h],eax 
                                                        0000000e  xor         eax,eax 
                                                        00000010  mov         dword ptr [ebp-1Ch],eax 
                                                        00000013  mov         dword ptr [ebp-3Ch],ecx 
                                                        00000016  cmp         dword ptr ds:[00A2853Ch],0 
                                                        0000001d  je          00000024 
                                                        0000001f  call        793B716F 
                                                        00000024  fldz             
                                                        00000026  fstp        dword ptr [ebp-40h] 
                                                        00000029  fldz             
                                                        0000002b  fstp        dword ptr [ebp-44h] 
                                                        0000002e  xor         esi,esi 
                                                        00000030  nop              
      float                                                      float
        a = float.MaxValue / 3.0f,                                a = float.MaxValue / 3.0f,
00000000  sub         esp,0Ch                            00000031  mov         dword ptr [ebp-40h],7EAAAAAAh
00000003  mov         dword ptr [esp],ecx                
00000006  cmp         dword ptr ds:[00A2853Ch],0        
0000000d  je          00000014                            
0000000f  call        793B716F                            
00000014  fldz                                            
00000016  fstp        dword ptr [esp+4]                    
0000001a  fldz                                            
0000001c  fstp        dword ptr [esp+8]                    
00000020  mov         dword ptr [esp+4],7EAAAAAAh        
        b = a * a;                                                b = a * a;
00000028  fld         dword ptr [esp+4]                    00000038  fld         dword ptr [ebp-40h] 
0000002c  fmul        st,st(0)                            0000003b  fmul        st,st(0) 
0000002e  fstp        dword ptr [esp+8]                    0000003d  fstp        dword ptr [ebp-44h] 

      if (a * a < b)                                          if (a * a < b)
00000032  fld         dword ptr [esp+4]                    00000040  fld         dword ptr [ebp-40h] 
00000036  fmul        st,st(0)                            00000043  fmul        st,st(0) 
00000038  fld         dword ptr [esp+8]                    00000045  fld         dword ptr [ebp-44h] 
0000003c  fcomip      st,st(1)                            00000048  fcomip      st,st(1) 
0000003e  fstp        st(0)                                0000004a  fstp        st(0) 
00000040  jp          00000054                            0000004c  jp          00000052 
00000042  jbe         00000054                            0000004e  ja          00000056 
                                                        00000050  jmp         00000052 
                                                        00000052  xor         eax,eax 
                                                        00000054  jmp         0000005B 
                                                        00000056  mov         eax,1 
                                                        0000005b  test        eax,eax 
                                                        0000005d  sete        al   
                                                        00000060  movzx       eax,al 
                                                        00000063  mov         esi,eax 
                                                        00000065  test        esi,esi 
                                                        00000067  jne         0000007A 
      {                                                          {
        Console.WriteLine ("Less");                        00000069  nop              
00000044  mov         ecx,dword ptr ds:[0239307Ch]                Console.WriteLine ("Less");
0000004a  call        78678B7C                            0000006a  mov         ecx,dword ptr ds:[0239307Ch] 
0000004f  nop                                            00000070  call        78678B7C 
00000050  add         esp,0Ch                            00000075  nop              
00000053  ret                                                  }
      }                                                    00000076  nop              
      else                                                00000077  nop              
      {                                                    00000078  jmp         00000088 
        Console.WriteLine ("GreaterEqual");                      else
00000054  mov         ecx,dword ptr ds:[02393080h]              {
0000005a  call        78678B7C                            0000007a  nop              
      }                                                            Console.WriteLine ("GreaterEqual");
    }                                                    0000007b  mov         ecx,dword ptr ds:[02393080h] 
                                                        00000081  call        78678B7C 
                                                        00000086  nop              
                                                              }

Lo que muestra lo anterior es que el código de punto flotante es el mismo tanto para la depuración como para el lanzamiento, el compilador elige la coherencia en lugar de la optimización.Aunque el programa produce un resultado incorrecto (a * a no es menor que b), es el mismo independientemente del modo de depuración/liberación.

Ahora, la FPU Intel IA32 tiene ocho registros de punto flotante, se podría pensar que el compilador usaría los registros para almacenar valores al optimizar en lugar de escribir en la memoria, mejorando así el rendimiento, algo como:

fld         dword ptr [a] ; precomputed value stored in ram == float.MaxValue / 3.0f
fmul        st,st(0) ; b = a * a
; no store to ram, keep b in FPU
fld         dword ptr [a]
fmul        st,st(0)
fcomi       st,st(0) ; a*a compared to b

pero esto se ejecutaría de manera diferente a la versión de depuración (en este caso, mostraría el resultado correcto).Sin embargo, cambiar el comportamiento del programa según las opciones de compilación es algo muy malo.

El código FPU es un área en la que la elaboración manual del código puede superar significativamente al compilador, pero es necesario comprender cómo funciona la FPU.

De hecho, pueden diferir si el modo de depuración usa la FPU x87 y el modo de lanzamiento usa SSE para operaciones flotantes.

En respuesta a la solicitud anterior de Frank Krueger (en los comentarios) de una demostración de una diferencia:

Compile este código en GCC sin optimizaciones y -mfpmath = 387 (no tengo ninguna razón para pensar que no funcionaría en otros compiladores, pero no lo he probado). Luego compile sin optimizaciones y -msse -mfpmath = Sse.

La salida será diferente.

#include <stdio.h>

int main()
{
    float e = 0.000000001;
    float f[3] = {33810340466158.90625,276553805316035.1875,10413022032824338432.0};
    f[0] = pow(f[0],2-e); f[1] = pow(f[1],2+e); f[2] = pow(f[2],-2-e);
    printf("%s\n",f);
    return 0;
}

Aquí hay un ejemplo simple donde los resultados no solo difieren entre el modo de depuración y el modo de lanzamiento, sino que la forma en que lo hacen depende de si se usa x86 o x84 como plataforma:

Single f1 = 0.00000000002f;
Single f2 = 1 / f1;
Double d = f2;
Console.WriteLine(d);

Esto escribe los siguientes resultados:

            Debug       Release
x86   49999998976   50000000199,7901
x64   49999998976   49999998976

Un vistazo rápido al desensamblado (Depurar -> Windows -> Desensamblado en Visual Studio) brinda algunas pistas sobre lo que está sucediendo aquí.Para el caso x86:

Debug                                       Release
mov         dword ptr [ebp-40h],2DAFEBFFh | mov         dword ptr [ebp-4],2DAFEBFFh  
fld         dword ptr [ebp-40h]           | fld         dword ptr [ebp-4]   
fld1                                      | fld1
fdivrp      st(1),st                      | fdivrp      st(1),st
fstp        dword ptr [ebp-44h]           |
fld         dword ptr [ebp-44h]           |
fstp        qword ptr [ebp-4Ch]           |
fld         qword ptr [ebp-4Ch]           |
sub         esp,8                         | sub         esp,8 
fstp        qword ptr [esp]               | fstp        qword ptr [esp]
call        6B9783BC                      | call        6B9783BC

En particular, vemos que un montón de elementos aparentemente redundantes de "almacenar el valor del registro de punto flotante en la memoria, luego cargarlo inmediatamente de nuevo desde la memoria al registro de punto flotante" se han optimizado en el modo de lanzamiento.Sin embargo, las dos instrucciones

fstp        dword ptr [ebp-44h]  
fld         dword ptr [ebp-44h]

son suficientes para cambiar el valor en el registro x87 de +5.0000000199790138e+0010 a +4.9999998976000000e+0010, como se puede verificar siguiendo el desmontaje e investigando los valores de los registros relevantes (Depurar -> Windows -> Registros, luego a la derecha haga clic y marque "Punto flotante").

La historia para x64 es tremendamente diferente.Seguimos viendo la misma optimización eliminando algunas instrucciones, pero esta vez, todo depende de SSE con sus registros de 128 bits y su conjunto de instrucciones dedicado:

Debug                                        Release
vmovss      xmm0,dword ptr [7FF7D0E104F8h] | vmovss      xmm0,dword ptr [7FF7D0E304C8h]  
vmovss      dword ptr [rbp+34h],xmm0       | vmovss      dword ptr [rbp-4],xmm0 
vmovss      xmm0,dword ptr [7FF7D0E104FCh] | vmovss      xmm0,dword ptr [7FF7D0E304CCh]
vdivss      xmm0,xmm0,dword ptr [rbp+34h]  | vdivss      xmm0,xmm0,dword ptr [rbp-4]
vmovss      dword ptr [rbp+30h],xmm0       |
vcvtss2sd   xmm0,xmm0,dword ptr [rbp+30h]  | vcvtss2sd   xmm0,xmm0,xmm0 
vmovsd      qword ptr [rbp+28h],xmm0       |
vmovsd      xmm0,qword ptr [rbp+28h]       |
call        00007FF81C9343F0               | call        00007FF81C9343F0 

Aquí, debido a que la unidad SSE evita el uso interno de una precisión mayor que la precisión simple (mientras que la unidad x87 lo hace), terminamos con el resultado de "precisión simple" del caso x86 independientemente de las optimizaciones.De hecho, uno encuentra (después de habilitar los registros SSE en la descripción general de Registros de Visual Studio) que después vdivss, XMM0 contiene 0000000000000000-00000000513A43B7 que es exactamente el 49999998976 de antes.

Ambas discrepancias me afectaron en la práctica.Además de ilustrar que nunca se debe comparar la igualdad de puntos flotantes, el ejemplo también muestra que todavía hay espacio para la depuración de ensambladores en un lenguaje de alto nivel como C#, en el momento en que aparecen los puntos flotantes.

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