Newsgroup: it.comp.lang.c
Titolo: Rappresentazione binaria di floating

Domanda idiota: come si rappresenta un valore floating-point nella
notazione binaria?
Grazie in anticipo.
-- 
-------------------------------------------------------------------
     Roberto Tagliarino
 
roberto.t...@multiwire.net
-------------------------------------------------------------------

notazione binaria?
Per carita`, tutt'altro che idiota!  La maggior parte dei computer
usano oggi la rappresentazione standard IEEE: per un valore
float, questo significa 32 bit, composti di:
    -- un bit di segno,
    -- 8 bit di esponente binario "excess 127",
    -- 23 bit di significante (spesso chiamato erroneamente
        "mantissa")
e per un valore double, analogamente ma con
    -- 11 bit di esponente "excess 1023", e
    -- 52 bit di significante
Il significante rappresenta di per se` un numero floating
point, fra 1.0 incluso e 2.0 escluso, che e` quello che si
otterrebbe dividendo o moltiplicando il numero che
stiamo rappresentando per opportuna potenza di due.
L'esponente indica appunto questa potenza di due; e`
registrato sempre come numero positivo, grazie al
"trucco" di aggiungere, al vero esponente della base
due nella notazione di riferimento:
    valore = segno * (2 alla esponente) * significante
un "eccesso" di 127 o 1023 rispettivamente.
Ulteriore trucco e` che, visto che il significante e`
tale per definizione da avere sempre il bit piu`
alto settato, quel bit non viene registrato nella
memorizzazione della rappresentazione (quindi
i 23 bit di significante in singola precisione fanno
il "lavoro" di 24, e i 52 in doppia precisione quello
di 53).
Alcune pattern di bit sono riservate per denotare
lo zero (che in questa notazione non troverebbe
altrimenti posto) e vari "non-numeri" importanti
nei calcoli, come ad esempio l'infinito (o i due
infiniti, negativo e positivo, se si adotta l'approccio
"affine" invece che quello "proiettivo" alla gestione
dell'infinito stesso).
Ecco un programmino giocattolo che facilita
l'esame di queste rappresentazioni:
#include <string.h>
#include <stdio.h>
void showbyte(unsigned char uc)
{
    int i;
    for(i=0; i<8; ++i) {
        if(uc&1) putchar('1');
        else putchar('0');
        uc >>= 1;
    }
    putchar(' ');
}
void show(float f)
{
    int i;
    unsigned char uc[4];
    memcpy(uc, &f, 4);
    for(i=0; i<4; ++i)
        showbyte(uc[i]);
    printf("%16.12g\n", f);
}
int main(void)
{
    show(0.0);
    show(1.0);
    show(2.0);
    show(4.0);
    show(8.0);
    return 0;
}
Questo stampa i bit dal meno significativo sino
al piu` significativo, poiche` e` pensato per l'uso
su macchine "little Endian" come Intel e Alpha;
quindi vedremo a sinistra i bit del significante,
dal meno al piu` significativo (ricordiamo che il
piu` significativo di tutti e` "sempre" 1, quindi
non risulta nella stampa!-), poi quelli dell'esponente
(che viene ad occupare l'ultimo bit piu` a destra
del terzo byte, poi i primi sette del quarto), e
infine il segno (zero per numeri positivi).
In questo modo si nota ad esempio che la
pattern speciale che rappresenta lo zero e`
quella con tutti i bit a zero (strano eh?-) e si
puo` sperimentare mostrando numeri vari
per capire meglio i concetti dell'IEEE standard
per la rappresentazione dei float.
La funzione standard frexp() puo` pure
aiutare, ma essa "scompone" un numero
double a un piu` alto livello di astrazione:
il significante e` posto fra 0.5 e 1.0, con
il suo bit piu` significativo "reintegrato", e
all'esponente e` gia` applicato l'excess in
modo da vederlo come intero con segno.
Alex

On Sun, 11 Oct 1998 10:00:07 +0200 Alex Martelli wrote:
: [...]  La maggior parte dei computer
: usano oggi la rappresentazione standard IEEE [...]
Pero' ...
Questo spiega perche' i calcoli coi valori floating sono molto piu'
*pesanti* di quelli a valore intero.
D'ora in avanti usero' con parsimonia i float e i double :)
Grazie della risposta.


D'ora in avanti usero' con parsimonia i float e i double :)
Beh, "molto" piu` pesanti, non esageriamo... dipende
molto dalla esatta CPU in gioco, ma su alcune delle
moderne CPU non e` poi tanto vero che i calcoli float
siano "molto piu` pesanti" di quelli int!  Tieni presente
che c'e` nella CPU parecchio hardware dedicato per
i calcoli float, e qualcosa conta!  Ad esempio, sul mio
AMD K6 casalingo, ecco i tempi in secondi di varie
esecuzioni di 500,000 addizioni e 500,000 moltiplicazioni
int e float (misurate con QueryPerformanceX di Win98):
int: 0.204077, float: 0.257604
int: 0.225057, float: 0.211572
int: 0.233923, float: 0.194024
int: 0.183669, float: 0.188519
int: 0.189649, float: 0.189642
int: 0.187134, float: 0.189718
int: 0.184681, float: 0.191436
int: 0.207158, float: 0.250419
int: 0.184748, float: 0.189908
int: 0.185812, float: 0.190775
int: 0.185783, float: 0.189054
int: 0.183396, float: 0.192039
int: 0.185832, float: 0.192737
int: 0.184799, float: 0.188721
int: 0.186003, float: 0.191693
int: 0.183017, float: 0.192692
int: 0.184195, float: 0.188601
int: 0.183178, float: 0.195149
int: 0.182363, float: 0.192005
int: 0.182258, float: 0.189861
Notiamo che, si`, c'e` un _piccolo_ vantaggio per
il calcolo int, ma non e` poi gran che; prendendo i
tempi migliori come piu` rappresentativi (visto che
tutti i tempi comprendono piccoli ma variabili
overhead di sistema operativo), 183.017 msec
per gli int, 188.519 per i float, e` poi solo il 3%
circa di rallentamento -- da tener d'occhio, certo,
ma se e` questo il "collo di bottiglia" delle attuali
prestazioni del tuo programma, vuol certo dire
che di ottimizzazioni ce ne hai gia` fatte TANTE...!
E notiamo accuratamente cosa succede quando
ci servono i 64 bit, quindi con __int64 come tipo
intero e double come tipo flottante...:
int: 0.408596, float: 0.437358
int: 0.398679, float: 0.388728
int: 0.399312, float: 0.401203
int: 0.400507, float: 0.385815
int: 0.401367, float: 0.3861
int: 0.393428, float: 0.386473
int: 0.396321, float: 0.38701
int: 0.392798, float: 0.388464
int: 0.397532, float: 0.385111
int: 0.39246, float: 0.389965
int: 0.397473, float: 0.386028
int: 0.394873, float: 0.388935
int: 0.39713, float: 0.385422
int: 0.399591, float: 0.384318
int: 0.398847, float: 0.388462
int: 0.395827, float: 0.388581
int: 0.395535, float: 0.389285
int: 0.397447, float: 0.38913
int: 0.398165, float: 0.386771
int: 0.399072, float: 0.386673
Qui abbiamo 392.46 come miglior prestazione
dell'__int64, contro 384.318 come migliore con
il double -- il vantaggio, proporzionamente simile,
e` ora dunque per il _flottante_...!
Per riferimento, ecco il programmino che ho
usato per la misura dei tempi (senza ottimizzare,
naturalmente, per garantire che i calcoli fossero
effettuati tutti) -- chiedo scusa se e` in C++, non
appropriato per il gruppo, ma i template fanno,
come al solito molto comodo, e sono certo che
nessuno avra` problemi a trascriverlo in C per
provare su altre CPU...:
#include <iostream>
using std::cout;
#include <windows.h>
template<class T>
class calcola {
public:
    T* pt;
    int n;
    calcola(int n): pt(0), n(n) {
        pt = new T[n];
    }
    ~calcola() { delete[] pt; }
    void iota() {
        int j;
        for(T i=j=0; j<n; ++j, ++i)
            pt[j] = i;
    }
    void add(T t) {
        for(int i=0; i<n; ++i)
            pt[i] += t;
    }
    void mul(T t) {
        for(int i=0; i<n; ++i)
            pt[i] *= t;
    }
};
class timer {
    LARGE_INTEGER lit;
    double sofar;
public:
    timer(): sofar(0) {
        lit.QuadPart = 0;
    }
    void start() {
        if(!QueryPerformanceCounter(&lit)) {
            throw "can't QPC!";
        }
    }
    void stop() {
        LARGE_INTEGER lat;
        if(!QueryPerformanceCounter(&lat)) {
            throw "can't QPC 2nd!";
        }
        sofar = (double)(lat.QuadPart-lit.QuadPart);
    }
    double tim() {
        LARGE_INTEGER fre;
        if(!QueryPerformanceFrequency(&fre)) {
            throw "can't QPC 2nd!";
        }
        return sofar/(double)fre.QuadPart;
    }
};
template <class T>
double calctime(long ito)
{
    calcola<T> ico(ito);
    timer ti;
    ti.start();
    ico.iota();
    ico.add(23);
    ico.mul(7);
    ti.stop();
    return ti.tim();
}
template <class T1, class T2>
void doit(long ito)
{
    cout<<"int: "<<calctime<T1>(ito);
    cout<<" float: "<<calctime<T2>(ito);
    cout<<"\n";
}
int main()
{
    for(int i=0; i<20; ++i)
        doit<int,float>(5*100*1000);
    return 0;
}
Alex