Perché un risultato di divisione differisce in base al tipo di getto? (Azione supplementare)

Questo è il seguito di questa domanda: perché un risultato di divisione differisce in base al tipo di cast?

Riepilogo rapido:

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 sono diversi a seconda del tipo di cast? 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)); 

Questo produce quanto segue:

 01000010 10100000 00000000 00000000 

Abbattendolo nel formato IEEE 754:

 0 10000101 01000000000000000000000 

Cartello:

 0 => Positive 

Esponente:

 10000101 => 133 in base 10 

mantissa:

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

Rappresentazione decimale:

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

Ne risultano esattamente 80. Quindi perché la fusione del risultato fa la differenza?

La mia risposta nell’altra discussione non è del tutto corretta: in realtà, quando calcasting in fase di esecuzione, (byte)(64 / 0.8f) è 80 .

Quando si esegue il casting di un float contenente il risultato di 64 / 0.8f , a byte in fase di esecuzione, il risultato è effettivamente 80. Tuttavia, questo non è il caso quando il cast viene eseguito come 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 è distriggersto. Secondo lo sassembly, 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 che Pascal Cuoq ha descritto: Durante la compilazione, il compilatore C # usa il double per calcolare l’espressione. Questo 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 “a virgola mobile” non si trova nell’intervallo di un float.

Durante il runtime, questo stampa anche 79:

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

EDIT2: come richiesto da Pascal Cuoq, ho eseguito il seguente codice:

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

Il risultato è 79. Quindi l’affermazione sopra riportata che il compilatore e il runtime calcolano un risultato diverso non è vero.

EDIT3 : Quando si modifica il codice precedente in (crediti per Pascal Cuoq, di nuovo), il risultato è 80:

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

Si noti, tuttavia, che questo non è il caso durante la scrittura (risultati in 79):

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

Quindi ecco cosa sembra accadere: (byte)(64 / 0.8f) non viene valutato come float , ma valutato come double (prima di convertirlo in 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 il tempo di compilazione (ansible quando si utilizzano solo le costanti), il cast esplicito per il float sembra essere ignorato / ottimizzato.

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

La specifica del linguaggio C # consente di calcolare risultati intermedi in virgola mobile con una precisione superiore a quella del tipo . Questo è molto probabile cosa sta succedendo qui.

Mentre il 64 / 0.8 calcolato con una precisione più elevata è leggermente inferiore a 80 (perché 0,8 non può essere rappresentato esattamente in virgola mobile binario) e converte in 79 quando viene troncato in un tipo intero, se il risultato della divisione viene convertito in 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 valore predefinito “al più vicino”. Le conversioni da virgola mobile a tipi interi troncano).

Anche se C # segue il comando di Java (IMHO sfortunato) nel richiedere un cast esplicito in qualsiasi momento qualcosa che viene specificato come double viene memorizzato in un float , il codice generato dal compilatore C # consente a .NET Runtime di eseguire calcoli come double e usare quei valori double in molti contesti in cui il tipo di espressione dovrebbe, a seconda delle regole linguistiche, essere float .

Fortunatamente, il compilatore C # offre almeno un modo per assicurare che le cose che dovrebbero essere arrotondate al float rappresentativo più vicino siano: lanciarle esplicitamente sul float .

Se scrivi la tua espressione come (byte)(float)(sixtyFour / 0.8f) , questo dovrebbe forzare il risultato ad arrotondarsi al valore float rappresentabile più vicino prima di troncare la parte frazionaria. Sebbene il cast da float possa sembrare ridondante (il tipo di espressione in fase di compilazione è già float ), il cast trasformsrà “cosa che dovrebbe essere float ma in realtà è double ” in qualcosa che in realtà è un float .

Storicamente, alcune lingue specificano che tutte le operazioni in virgola mobile vengono eseguite sul tipo double ; float esisteva non per velocizzare i calcoli, ma per ridurre i requisiti di archiviazione. Generalmente non era necessario specificare le costanti come tipo float , poiché la divisione per 0.800000000000000044 (il double valore 0.8) non era più lenta della divisione per 0.800000011920929 (il valore 0.8f ). C # in qualche modo fastidiosamente non permetterà float1 = float2 / 0.8; a causa della “perdita di precisione” ma preferisce invece il meno preciso float1 = float2 / 0.8f; e non fa nemmeno caso al double1 = float1 / 0.8f; probabilmente erroneo double1 = float1 / 0.8f; . Il fatto che le operazioni vengano eseguite tra valori float non significa che il risultato sarà in realtà un float , ma – significa semplicemente che il compilatore consentirà di essere arrotondato in modo silenzioso a un float in alcuni contesti ma non lo forzerà in altri .