Domanda

Le operazioni in virgola mobile C#/.NET differiscono in termini di precisione tra la modalità di debug e la modalità di rilascio?

È stato utile?

Soluzione

Possono infatti essere diversi.Secondo la specifica CLR ECMA:

Le posizioni di archiviazione per numeri a virgola mobile (statica, elementi di array e campi delle classi) sono di dimensioni fisse.Le dimensioni di archiviazione supportate sono float32 e float64.Ovunque altro (nello stack di valutazione, come argomenti, come tipi di restituzione e come variabili locali) i numeri a punto mobile sono rappresentati usando un tipo di punto mobile interno.In ogni istanza, il tipo nominale della variabile o dell'espressione è R4 o R8, ma il suo valore può essere rappresentato internamente con intervallo aggiuntivo e/o precisione.La dimensione della rappresentazione del punto mobile interno è dipendente dall'implementazione, può variare e deve avere precisione almeno tanto quanto quella della variabile o dell'espressione rappresentata.Una conversione implicita di ampliamento alla rappresentazione interna da Float32 o Float64 viene eseguita quando tali tipi vengono caricati dalla memoria.La rappresentazione interna è in genere la dimensione nativa per l'hardware o come richiesto per un'implementazione efficiente di un'operazione.

Ciò significa sostanzialmente che il seguente confronto può o meno essere uguale:

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?).
    }
  }
}

Messaggio da portare a casa:non controllare mai l'uguaglianza dei valori mobili.

Altri suggerimenti

Questa è una domanda interessante, quindi ho fatto un po' di sperimentazione.Ho usato questo codice:

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");
  }
}

utilizzando DevStudio 2005 e .Net 2.Ho compilato sia come debug che come rilascio ed ho esaminato l'output del compilatore:

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              
                                                              }

Ciò che mostra quanto sopra è che il codice in virgola mobile è lo stesso sia per il debug che per il rilascio, il compilatore sceglie la coerenza rispetto all'ottimizzazione.Sebbene il programma produca il risultato sbagliato (a * a non è inferiore a b) è lo stesso indipendentemente dalla modalità debug/rilascio.

Ora, la FPU Intel IA32 ha otto registri in virgola mobile, si potrebbe pensare che il compilatore utilizzi i registri per memorizzare valori durante l'ottimizzazione anziché scrivere in memoria, migliorando così le prestazioni, qualcosa sulla falsariga di:

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

ma questo verrebbe eseguito in modo diverso rispetto alla versione di debug (in questo caso, visualizzerebbe il risultato corretto).Tuttavia, modificare il comportamento del programma in base alle opzioni di compilazione è una cosa molto negativa.

Il codice FPU è un'area in cui la creazione manuale del codice può superare in modo significativo le prestazioni del compilatore, ma è necessario capire come funziona la FPU.

In effetti, potrebbero differire se la modalità di debug utilizza la FPU x87 e la modalità di rilascio utilizza SSE per le operazioni float.

In risposta alla richiesta di Frank Krueger sopra (nei commenti) per una dimostrazione di una differenza:

Compila questo codice in GCC senza ottimizzazioni e -mfpmath = 387 (non ho motivo di pensare che non funzionerebbe su altri compilatori, ma non l'ho provato.) Quindi compilalo senza ottimizzazioni e -msse -mfpmath = SSE.

L'output sarà diverso.

#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;
}

Ecco un semplice esempio in cui i risultati non solo differiscono tra la modalità di debug e quella di rilascio, ma il modo in cui lo fanno dipende dall'utilizzo di x86 o x84 come piattaforma:

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

Questo scrive i seguenti risultati:

            Debug       Release
x86   49999998976   50000000199,7901
x64   49999998976   49999998976

Un rapido sguardo al disassemblaggio (Debug -> Windows -> Disassembly in Visual Studio) fornisce alcuni suggerimenti su cosa sta succedendo qui.Per il 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

In particolare, vediamo che un gruppo apparentemente ridondante di "memorizza il valore dal registro a virgola mobile in memoria, quindi caricalo immediatamente dalla memoria nel registro a virgola mobile" è stato ottimizzato nella modalità di rilascio.Tuttavia, le due istruzioni

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

sono sufficienti per modificare il valore nel registro x87 da +5.0000000199790138e+0010 a +4.9999998976000000e+0010 come si può verificare procedendo nel disassemblaggio ed esaminando i valori dei relativi registri (Debug -> Windows -> Registri, quindi a destra fare clic e selezionare "Virgola mobile").

La storia per x64 è molto diversa.Vediamo ancora la stessa ottimizzazione rimuovendo alcune istruzioni, ma questa volta tutto si basa su SSE con i suoi registri a 128 bit e un set di istruzioni dedicato:

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 

Qui, poiché l'unità SSE evita di utilizzare internamente una precisione più elevata rispetto alla precisione singola (mentre l'unità x87 lo fa), ci ritroviamo con il risultato "precisione singola" del caso x86 indipendentemente dalle ottimizzazioni.In effetti, si trova (dopo aver abilitato i registri SSE nella panoramica dei registri di Visual Studio) che after vdivss, XMM0 contiene 0000000000000000-00000000513A43B7 che è esattamente il 49999998976 di prima.

Entrambe le discrepanze mi hanno ferito nella pratica.Oltre a illustrare che non si dovrebbe mai confrontare l'uguaglianza dei punti mobili, l'esempio mostra anche che c'è ancora spazio per il debugging dell'assembly in un linguaggio di alto livello come C#, nel momento in cui vengono visualizzati i punti mobili.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top