Espressione float C #: strano comportamento durante il cast del risultato float su int


128

Ho il seguente semplice codice:

int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;

speed1e speed2dovrebbe avere lo stesso valore, ma in realtà ho:

speed1 = 61
speed2 = 62

So che dovrei probabilmente usare Math.Round invece del casting, ma vorrei capire perché i valori sono diversi.

Ho esaminato il bytecode generato, ma tranne un negozio e un carico, i codici operativi sono gli stessi.

Ho anche provato lo stesso codice in Java e ottengo correttamente 62 e 62.

Qualcuno può spiegare questo?

Modifica: nel codice reale, non è direttamente 6.2f * 10 ma una chiamata di funzione * una costante. Ho il seguente bytecode:

per speed1:

IL_01b3:  ldloc.s    V_8
IL_01b5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ba:  ldc.r4     10.
IL_01bf:  mul
IL_01c0:  conv.i4
IL_01c1:  stloc.s    V_9

per speed2:

IL_01c3:  ldloc.s    V_8
IL_01c5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ca:  ldc.r4     10.
IL_01cf:  mul
IL_01d0:  stloc.s    V_10
IL_01d2:  ldloc.s    V_10
IL_01d4:  conv.i4
IL_01d5:  stloc.s    V_11

possiamo vedere che gli operandi sono float e che l'unica differenza è il stloc/ldloc.

Per quanto riguarda la macchina virtuale, ho provato con Mono / Win7, Mono / MacOS e .NET / Windows, con gli stessi risultati.


9
La mia ipotesi è che una delle operazioni sia stata eseguita con precisione singola mentre l'altra sia stata eseguita con precisione doppia. Uno di questi ha restituito valori leggermente inferiori a 62, ottenendo quindi 61 quando si troncava a un numero intero.
Gabe,

2
Questi sono tipici problemi di precisione in virgola mobile.
TJHeuvel,

3
Provando questo su .Net / WinXP, .Net / Win7, Mono / Ubuntu e Mono / OSX si ottengono i risultati per entrambe le versioni di Windows, ma 62 per speed1 e speed2 in entrambe le versioni Mono. Grazie @BoltClock
Eugen Rieck,

6
Signor Lippert ... lei in giro ??
vc 74

6
Il valutatore di espressioni costanti del compilatore non sta vincendo alcun premio qui. Chiaramente sta troncando 6.2f nella prima espressione, non ha una rappresentazione esatta nella base 2, quindi finisce con 6.199999. Ma non lo fa nella seconda espressione, probabilmente riuscendo a mantenerlo in doppia precisione in qualche modo. Questo è altrimenti alla pari per il corso, la coerenza in virgola mobile non è mai un problema. Questo non verrà risolto, conosci la soluzione alternativa.
Hans Passant,

Risposte:


168

Prima di tutto, suppongo che tu sappia che 6.2f * 10non è esattamente 62 a causa dell'arrotondamento in virgola mobile (è in realtà il valore 61.99999809265137 quando espresso come a double) e che la tua domanda riguarda solo perché due calcoli apparentemente identici danno come risultato un valore errato.

La risposta è che nel caso di (int)(6.2f * 10), stai prendendo il doublevalore 61.99999809265137 e troncandolo a un numero intero, che produce 61.

Nel caso di float f = 6.2f * 10, stai prendendo il doppio valore 61.99999809265137 e arrotondando al più vicino float, che è 62. Quindi lo tronchi floata un numero intero e il risultato è 62.

Esercizio: spiega i risultati della seguente sequenza di operazioni.

double d = 6.2f * 10;
int tmp2 = (int)d;
// evaluate tmp2

Aggiornamento: come notato nei commenti, l'espressione 6.2f * 10è formalmente a floatpoiché il secondo parametro ha una conversione implicita in floatcui è migliore della conversione implicita in double.

Il vero problema è che al compilatore è permesso (ma non richiesto) di usare un intermedio che ha una precisione superiore rispetto al tipo formale (sezione 11.2.2) . Ecco perché vedi comportamenti diversi su sistemi diversi: nell'espressione (int)(6.2f * 10), il compilatore ha l'opzione di mantenere il valore 6.2f * 10in una forma intermedia di alta precisione prima di convertirlo in int. In caso affermativo, il risultato è 61. In caso contrario, il risultato è 62.

Nel secondo esempio, l'assegnazione esplicita per floatforzare l'arrotondamento ha luogo prima della conversione in numero intero.


6
Non sono sicuro che questo risponda effettivamente alla domanda. Perché sta (int)(6.2f * 10)prendendo il doublevalore, come fspecifica che è un float? Penso che il punto principale (ancora senza risposta) sia qui.
ken2k,

1
Penso che sia il compilatore che lo sta facendo, dal momento che è float letteral * int letteral il compilatore ha deciso che è libero di usare il miglior tipo numerico e per risparmiare precisione è andato per il doppio (forse). (spiegherebbe anche IL che è lo stesso)
George Duckett il

5
Buon punto. Il tipo di 6.2f * 10è in realtà float, no double. Penso che il compilatore stia ottimizzando l'intermedio, come consentito dall'ultimo paragrafo dell'11.1.6 .
Raymond Chen,

3
Ha lo stesso valore (il valore è 61.99999809265137). La differenza è il percorso che il valore intraprende per diventare un numero intero. In un caso, passa direttamente a un numero intero e in un altro passa prima attraverso una floatconversione.
Raymond Chen,

38
La risposta di Raymond qui è ovviamente del tutto corretta. Prendo atto che il compilatore C # e il compilatore jit possono entrambi usare più precisione in qualsiasi momento e farlo in modo incoerente . E in effetti, fanno proprio questo. Questa domanda è emersa decine di volte su StackOverflow; vedi stackoverflow.com/questions/8795550/… per un esempio recente.
Eric Lippert,

11

Descrizione

Numeri mobili raramente esatti. 6.2fè qualcosa di simile 6.1999998.... Se lo lanci su un int, lo troncerà e questo * 10 si tradurrà in 61.

Dai un'occhiata alla DoubleConverterlezione di Jon Skeets . Con questa classe puoi davvero visualizzare il valore di un numero mobile come stringa. Doublee floatsono entrambi numeri mobili , il decimale non lo è (è un numero a virgola fissa).

Campione

DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875

Maggiori informazioni


5

Guarda IL:

IL_0000:  ldc.i4.s    3D              // speed1 = 61
IL_0002:  stloc.0
IL_0003:  ldc.r4      00 00 78 42     // tmp = 62.0f
IL_0008:  stloc.1
IL_0009:  ldloc.1
IL_000A:  conv.i4
IL_000B:  stloc.2

Il compilatore riduce le espressioni delle costanti in fase di compilazione al loro valore costante e penso che a un certo punto faccia un'approssimazione errata quando converte la costante in int. Nel caso di speed2, questa conversione non viene effettuata dal compilatore, ma dal CLR, e sembrano applicare regole diverse ...


1

La mia ipotesi è che 6.2fla rappresentazione reale con precisione float sia 6.1999999mentre 62fprobabilmente è qualcosa di simile 62.00000001. (int)il casting tronca sempre il valore decimale, quindi è per questo che si ottiene quel comportamento.

EDIT : Secondo i commenti ho riformulato il comportamento del intcasting con una definizione molto più precisa.


Il cast su un inttronca il valore decimale, non arrotonda.
Jim D'Angelo,

@James D'Angelo: mi dispiace che l'inglese non sia la mia lingua principale. Non conoscevo la parola esatta, quindi ho definito il comportamento come "arrotondamento per difetto quando si tratta di numeri positivi" che sostanzialmente descrive lo stesso comportamento. Ma sì, punto preso, troncare è la parola esatta per questo.
InB Between

nessun problema, è solo sintetico ma può causare problemi se qualcuno inizia a pensare float -> intcomporta l'arrotondamento. = D
Jim D'Angelo,

1

Ho compilato e disassemblato questo codice (su Win7 / .NET 4.0). Immagino che il compilatore valuti l'espressione costante mobile come doppia.

int speed1 = (int)(6.2f * 10);
   mov         dword ptr [rbp+8],3Dh       //result is precalculated (61)

float tmp = 6.2f * 10;
   movss       xmm0,dword ptr [000004E8h]  //precalculated (float format, xmm0=0x42780000 (62.0))
   movss       dword ptr [rbp+0Ch],xmm0 

int speed2 = (int)tmp;
   cvttss2si   eax,dword ptr [rbp+0Ch]     //instrunction converts float to Int32 (eax=62)
   mov         dword ptr [rbp+10h],eax 

0

Singlemantiene solo 7 cifre e quando lo si lancia su un Int32compilatore tronca tutte le cifre in virgola mobile. Durante la conversione si potrebbero perdere una o più cifre significative.

Int32 speed0 = (Int32)(6.2f * 100000000); 

fornisce il risultato di 619999980 quindi (Int32) (6.2f * 10) fornisce 61.

È diverso quando si moltiplicano due singoli, in tal caso non si verifica alcuna operazione troncata ma solo approssimazione.

Vedi http://msdn.microsoft.com/en-us/library/system.single.aspx


-4

C'è un motivo per cui stai scrivendo il casting intinvece di analizzare?

int speed1 = (int)(6.2f * 10)

avrebbe quindi letto

int speed1 = Int.Parse((6.2f * 10).ToString()); 

La differenza probabilmente ha a che fare con l'arrotondamento: se lanci su di doublete probabilmente otterrai qualcosa come 61.78426.

Si prega di notare il seguente output

int speed1 = (int)(6.2f * 10);//61
double speed2 = (6.2f * 10);//61.9999980926514

Ecco perché stai ottenendo valori diversi!


1
Int.Parseaccetta una stringa come parametro.
ken2k,

Puoi solo analizzare le stringhe, immagino che intendi perché non usi System.Convert
vc 74
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.