2014-09-06 8 views
5

Si tratta di un follow-up a questa domanda: Why does a division result differ based on the cast type?Perché un risultato di divisione differisce in base al tipo di getto? (Followup)

Sommario rapida:

byte b1 = (byte)(64/0.8f); // b1 is 79 
int b2 = (int)(64/0.8f); // b2 is 79 
float fl = (64/0.8f); // fl is 80 

La domanda è: Perché i risultati diversi a seconda del tipo di getto? Durante l'elaborazione di una risposta mi sono imbattuto in un problema che non ero in grado di spiegare.

var bytes = BitConverter.GetBytes(64/0.8f).Reverse(); // Reverse endianness 
var bits = bytes.Select(b => Convert.ToString(b, 2).PadLeft(8, '0')); 
Console.WriteLine(string.Join(" ", bits)); 

Questa emette il seguente:

01000010 10100000 00000000 00000000 

scomponendola in formato IEEE 754:

0 10000101 01000000000000000000000 

Segno:

0 => Positive 

Esponente:

10000101 => 133 in base 10 

Mantissa:

01000000000000000000000 => 0*2^-1 + 1*2^-2 + 0*2^-3 ... = 1/4 = 0.25 

decimale Rappresentazione:

(1 + 0.25) * 2^(133 - 127) (Subtract single precision bias) 

Questo si traduce in esattamente 80. Allora, perché non lanciare il risultato fare la differenza?

+3

Upvote per fare ulteriori ricerche - grande scoperta! – Matthias

risposta

4

mia risposta in altro thread non è del tutto corretto:. realtà, quando calcolata in fase di esecuzione, (byte)(64/0.8f) è 80.

Quando lancia un float contenente il risultato 64/0.8f, a byte in fase di esecuzione, il risultato effettivamente è 80. Tuttavia, questo non è il caso quando il cast è fatto come una parte del compito:

float f1 = (64/0.8f); 

byte b1 = (byte) f1; 
byte b2 = (byte)(64/0.8f); 

Console.WriteLine(b1); //80 
Console.WriteLine(b2); //79 

Mentre b1 contiene il risultato previsto, b2 è disattivato. Secondo lo smontaggio, b2 è assegnato come segue:

mov   dword ptr [ebp-48h],4Fh 

Pertanto, il compilatore sembra calcolare un risultato diverso dal risultato in fase di esecuzione. Non so, tuttavia, se questo è il comportamento previsto o meno.

EDIT: Forse è l'effetto Pascal Cuoq descritto: Durante il tempo di compilazione, il compilatore C# utilizza double per calcolare l'espressione. Ciò risulta in 79, xxx che viene troncato a 79 (come un doppio contiene abbastanza precisione da causare un problema, qui).
Usando float, tuttavia, non ci imbattiamo in alcun problema, poiché l'errore "virgola mobile" non si trova nell'intervallo di un float.

Durante il runtime, questa stampa anche 79:

double d1 = (64/0.8f); 
byte b3 = (byte) d1; 
Console.WriteLine(b3); //79 

EDIT2: Come di richiesta di Pascal Cuoq, ho eseguito il seguente codice:

int sixtyfour = Int32.Parse("64"); 
byte b4 = (byte)(sixtyfour/0.8f); 
Console.WriteLine(b4); //79 

risultato è 79. Così il sopra affermazione che il compilatore e il runtime calcolano un risultato diverso non è vero.

Edit3: Quando si modifica il codice precedente (crediti a Pascal Cuoq, ancora), il risultato è 80:

byte b5 = (byte)(float)(sixtyfour/0.8f); 
Console.WriteLine(b5); //80 

Si noti, tuttavia, che questo non è il caso quando si scrive (risultati in 79):

byte b6 = (byte)(float)(64/0.8f); 
Console.WriteLine(b6); //79 

ecco cosa sembra accadere: (byte)(64/0.8f) non viene valutato come un float, ma valutati come double (prima del getto a byte). Ciò provoca un errore di arrotondamento (che non si verifica quando il calcolo viene eseguito utilizzando float). Un cast esplicito da flottare prima di trasmettere a doppio (che è contrassegnato come ridondante da ReSharper, BTW) "risolve" questo problema. Tuttavia, quando il calcolo viene eseguito durante la compilazione (possibile quando si utilizzano solo le costanti), il cast esplicito a float sembra essere ignorato/ottimizzato.

TLDR: i calcoli in virgola mobile sono ancora più complicati di quanto sembrino inizialmente.

+1

non so la definizione esatta usata da C#, ma in altre lingue che permettono di precisione in più per i calcoli in virgola mobile intermedi, come C, ogni incarico ha per arrotondare alla precisione del tipo. Se C# funziona allo stesso modo, allora 'float f1 = (64/0.8f); byte b1 = (byte) f1; 'deve assegnare' f1' ai risultati arrotondati della divisione, e non ha bisogno di produrre lo stesso risultato di 'byte b2 = (byte) (64/0.8f);'. Il modo per verificare è che il 64 sia un input del programma, in modo che nessuno dei due sia ottimizzato. –

+0

Sembra che tu abbia ragione: l'ho appena fatto - il risultato è 79. Ora sono un po 'confuso, tuttavia, quello che sta realmente accadendo. – Matthias

+0

Penso di averlo capito ora con la mia ultima modifica - pensieri? – Matthias

3

Le specifiche del linguaggio C# consentono to compute intermediate floating-point results at a precision greater than that of the type. Questo è molto probabile cosa sta succedendo qui.

Mentre 64/0.8 calcolata con una precisione superiore è leggermente inferiore a 80 (perché 0.8cannot essere rappresentato esattamente in binario a virgola mobile), e converte 79 quando troncati a un tipo intero, se il risultato della divisione viene convertito float , è arrotondato a 80.0f.

(Le conversioni da virgola mobile a virgola mobile sono per il più vicino-tecnicamente, vengono eseguite in base alla modalità di arrotondamento della FPU, ma C# non consente di modificare la modalità di arrotondamento della FPU dal suo "al . vicina”default conversioni da virgola mobile a intero tipi troncare)

+0

Anche no - 79. – Matthias

+1

@ winSharp93 Arg! Stavo solo supponendo, però. Meglio rimuovere quella parte della risposta. –

+0

Sembra che tu abbia ragione, ma in un modo diverso (vedi la mia modifica): come "(byte) (64/0.8)" è una costante, il compilatore la valuta durante la compilazione. Questa ottimizzazione, tuttavia, viene eseguita utilizzando il doppio anziché il float per i calcoli. – Matthias

0

Anche se C# segue l'esempio di Java (IMHO sfortunato) nel richiedere un cast esplicito qualsiasi cosa il tempo che è specificato come double viene memorizzato un float, codice generato dal compilatore C# consente il runtime .NET per eseguire calcoli come double e utilizzare i valori double in molti contesti in cui il tipo di espressione deve, in base alle regole della lingua, essere float.

Fortunatamente, il compilatore C# non offre almeno un modo per garantire che le cose che si suppone essere arrotondati al più vicino rappresentabile float in realtà sono: li getteranno in modo esplicito a float.

Se si scrive l'espressione come (byte)(float)(sixtyFour/0.8f), che dovrebbe costringere il risultato di ottenere arrotondato al valore rappresentabile più vicino float prima di troncare la parte frazionaria. Anche se il cast al float può apparire ridondante (del tipo in fase di compilazione dell'espressione è già float), il cast girerà "cosa che si suppone essere float ma è davvero double" in qualcosa che in realtà è un float.

Storicamente, alcune lingue possono specificare che tutte le operazioni a virgola mobile vengono eseguiti tipo double; float esisteva non per velocizzare i calcoli, ma per ridurre i requisiti di archiviazione. Non c'era alcuna necessità di specificare costanti come tipo float, poiché dividendo per ,800000000000000044 (valore double 0.8) non era più lento di dividendo per ,800000011920929 (valore 0.8f). C# in qualche modo fastidiosamente non permetteranno float1 = float2/0.8; a causa della "perdita di precisione", ma invece favorisce il meno preciso float1 = float2/0.8f; e non ha nemmeno mente il probabile-erronea double1 = float1/0.8f;. Il fatto che le operazioni vengono eseguite tra float valori non significa il risultato sarà effettivamente un float, anche se - significa semplicemente il compilatore permetterà di essere silenziosamente arrotondato a una float in alcuni contesti ma non forzarlo in altri .

Problemi correlati