In che modo il computer esegue l’aritmetica in virgola mobile?

Ho visto lunghi articoli che spiegano come i numeri in virgola mobile possono essere memorizzati e come viene eseguita l’aritmetica di quei numeri, ma per favore spiegate brevemente perché quando scrivo

cout << 1.0 / 3.0 <<endl; 

Vedo 0.333333 , ma quando scrivo

 cout << 1.0 / 3.0 + 1.0 / 3.0 + 1.0 / 3.0 << endl; 

Vedo 1 .

Come fa il computer a fare questo? Per favore spiega solo questo semplice esempio. Mi basta.

Il problema è che il formato in virgola mobile rappresenta le frazioni nella base 2.

Il primo bit della frazione è ½, il secondo ¼ e continua come 1/2 n .

E il problema è che non tutti i numeri razionali (un numero che può essere express come il rapporto di due numeri interi) hanno effettivamente una rappresentazione finita in questo formato di base 2.

(Questo rende il formato in virgola mobile difficile da usare per i valori monetari.Tuttavia questi valori sono sempre numeri razionali ( n / 100) solo .00, .25, .50 e .75 hanno effettivamente rappresentazioni esatte in qualsiasi numero di cifre di un base due frazione).

Ad ogni modo, quando li aggiungi nuovamente, il sistema alla fine ottiene la possibilità di arrotondare il risultato a un numero che può rappresentare esattamente.

Ad un certo punto, si ritrova ad aggiungere il numero .666 … al .333 … uno, in questo modo:

  00111110 1 .o10101010 10101010 10101011 + 00111111 0 .10101010 10101010 10101011o ------------------------------------------ 00111111 1 (1).0000000 00000000 0000000x # the x isn't in the final result 

Il bit più a sinistra è il segno, gli otto successivi sono l’esponente e i bit rimanenti sono la frazione. Tra l’esponente e la frazione c’è un “1” presunto che è sempre presente, e quindi non effettivamente immagazzinato, come il bit della frazione più a sinistra normalizzato. Ho scritto degli zeri che non sono effettivamente presenti come bit individuali come o .

Qui è accaduto molto, ad ogni passo, la FPU ha adottato misure piuttosto eroiche per arrotondare il risultato. Sono state mantenute due cifre extra di precisione (oltre a quelle che si adattano al risultato) e l’FPU sa che in molti casi, se ce ne sono, o almeno 1, dei rimanenti bit più a destra è uno. Se è così, allora quella parte della frazione è più di 0,5 (in scala) e quindi arrotonda. I valori intermedi arrotondati consentono alla FPU di portare il bit più a destra fino alla parte intera e infine alla risposta corretta.

Questo non è accaduto perché qualcuno ha aggiunto 0,5; l’FPU ha appena fatto il meglio che poteva nei limiti del formato. Il virgola mobile non è, in realtà, inaccurato. È perfettamente preciso, ma la maggior parte dei numeri che ci aspettiamo di vedere nella nostra visione del mondo numero 10 basata sul numero razionale non sono rappresentabili dalla frazione di base 2 del formato. In effetti, pochissimi sono.

Leggi l’articolo su “Cosa dovrebbe sapere ogni scienziato informatico sull’aritmetica in virgola mobile”

Facciamo i conti. Per brevità, assumiamo che tu abbia solo quattro cifre significative (base-2).

Naturalmente, dal momento che gcd(2,3)=1 , 1/3 è periodico quando rappresentato in base-2. In particolare, non può essere rappresentato esattamente, quindi dobbiamo accontentarci dell’approssimazione

 A := 1×1/4 + 0×1/8 + 1×1/16 + 1*1/32 

che è più vicino al valore reale di 1/3 di

 A' := 1×1/4 + 0×1/8 + 1×1/16 + 0×1/32 

Quindi, la stampa di A in decimali fornisce 0,34375 (il fatto che nel tuo esempio si veda 0,33333 è semplicemente la prova del maggior numero di cifre significative in un double ).

Quando aggiungiamo questi tre volte, otteniamo

 A + A + A = ( A + A ) + A = ( (1/4 + 1/16 + 1/32) + (1/4 + 1/16 + 1/32) ) + (1/4 + 1/16 + 1/32) = ( 1/4 + 1/4 + 1/16 + 1/16 + 1/32 + 1/32 ) + (1/4 + 1/16 + 1/32) = ( 1/2 + 1/8 + 1/16 ) + (1/4 + 1/16 + 1/32) = 1/2 + 1/4 + 1/8 + 1/16 + 1/16 + O(1/32) 

Il termine O(1/32) non può essere rappresentato nel risultato, quindi è scartato e otteniamo

 A + A + A = 1/2 + 1/4 + 1/8 + 1/16 + 1/16 = 1 

QED 🙂

Per quanto riguarda questo specifico esempio: penso che i compilatori siano troppo intelligenti al giorno d’oggi e si assicurino automaticamente che un risultato const di tipi primitivi sia esatto se ansible. Non sono riuscito a ingannare g ++ nel fare un calcolo facile come questo sbagliato.

Tuttavia, è facile ignorare queste cose usando variabili non const. Ancora,

 int d = 3; float a = 1./d; std::cout << d*a; 

restituirà esattamente 1, anche se questo non dovrebbe essere previsto. Il motivo, come già detto, è che l' operator<< elimina l'errore.

Per quanto riguarda il motivo per cui può farlo: quando aggiungi numeri di dimensioni simili o moltiplica un float di un int , ottieni praticamente tutta la precisione che il float può offrire al massimo - ciò significa che il rapporto errore / risultato è molto piccolo ( in altre parole, gli errori si verificano in un decimale tardivo, assumendo che si sia verificato un errore positivo).

Quindi 3*(1./3) , anche se, come float, non esattamente ==1 , ha un grosso bias corretto che impedisce operator<< di prendersi cura dei piccoli errori. Tuttavia, se si rimuove questo errore semplicemente sottraendo 1, il punto mobile scivolerà verso il basso fino all'errore, e all'improvviso non sarà più trascurabile. Come ho detto, questo non succede se digiti semplicemente 3*(1./3)-1 perché il compilatore è troppo intelligente, ma prova

 int d = 3; float a = 1./d; std::cout << d*a << " - 1 = " << d*a - 1 << " ???\n"; 

Quello che ottengo è (g ++, 32 bit Linux)

 1 - 1 = 2.98023e-08 ??? 

Funziona perché la precisione predefinita è di 6 cifre e arrotondato a 6 cifre il risultato è 1. Vedi i costruttori di base 27.5.4.1 nello standard di bozza C ++ (n3092) .