Scrivere Software è un piacere. Un programma non solo deve funzionare bene ed essere efficiente (questo si dà per scontato), ma deve essere anche bello ed elegante da leggere, comprensibile e facile da manutenere, sia per l'autore che per eventuali lettori futuri. Programmare bene in C è un'arte.
Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...
Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.
venerdì 24 dicembre 2021
lunedì 20 dicembre 2021
Double Express come si comparano i double e i float in C
Ponchia: Ma non ti rovini i denti con tutta quella cioccolata?
Teresa: Ma ho un amico che è dentista.
Ponchia: Cioè, uno che ha un amico dentista allora si deve rovinare i denti. E se io avessi un amico patologo cosa dovrei fare?
Teresa: Ma ho un amico che è dentista.
Ponchia: Cioè, uno che ha un amico dentista allora si deve rovinare i denti. E se io avessi un amico patologo cosa dovrei fare?
La programmazione è un viaggio, un lungo viaggio alla ricerca del codice perfetto... esattamente come il viaggio nostalgico e catartico descritto nel bel Marrakech Express, un film che, metaforicamente, ci mostra la fuga dalla realtà e dal trantran quotidiano di un gruppo di amici. Un film brillantissimo che è ambientato in un momento storico e spensierato che mette un po' di nostalgia visto l'andazzo attuale. Ma bando alla tristezza: siamo qui per trattare di codice di alta qualità! Guardiamo avanti...
| ...ma non ti rovini il codice con tutti quei double?... |
Nell'ultimo articolo avevamo proseguito il nostro viaggio tra le nuove keyword del C99/C11 (abbiamo già visto la prima e la seconda parte, quindi toccherebbe la terza). Ma oggi voglio divagare un po', mi prendo una piccola pausa per cambiare di tema (sento già i sospiri di sollievo: ma siete così sicuri che non sarà una pizza questo nuovo argomento?).
E allora vediamo in po': mi sono accorto, mio malgrado, che nel nostro piccolo-grande mondo dei programmatori ci sono alcuni peccati originali veramente duri a morire. In alcuni articoli passati (ad esempio qui, qui o qui) ho cercato di smitizzarne un po', e oggi cercherò di scrivere la parola fine (si fa per dire...) su un argomento che, navigando in rete, ho notato che continua a generare molti dubbi, e non è che sia un tema poi così nuovo! E quindi, tanto per chiarire:
I DOUBLE (E I FLOAT) NON SI COMPARANO MAI USANDO L'OPERATORE "=="
e vi dirò di più: si dovrebbe anche diffidare degli operatori "<", "<=", ">", ">=" e "!=" ma questo proprio se vogliamo fare gli schizzinosi (e anche questo punto lo vedremo più avanti nell'articolo).
Come detto questa è una vecchia storia, vecchia come i Computer e il Software, ma è ancora sconosciuta a molti. Comunque, per i dubbiosi, facciamo cantare il codice, dove proveremo a comparare due numeri che sono certamente uguali (o no?):
#include <stdio.h>// main() - funzione mainint main(){// a e b dovrebbero valere entrambi 1.3...double a = (0.3 * 3.0) + 0.4;double b = 1.3;// ...e invece...printf("a = %.32f\n", a);printf("b = %.32f\n", b);// ...ecco la prova del disastro!if (a == b)printf("i numeri a e b sono uguali!\n");elseprintf("i numeri a e b NON sono uguali...\n");return 0;}
ecco, questo semplicissimo codice stampa questo:
a = 1.29999999999999982236431605997495b = 1.30000000000000004440892098500626i numeri a e b NON sono uguali...
Sorpresa! a e b NON sono uguali! Beh, in realtà non è una sorpresa, basta indagare un po su come sono rappresentati i numeri double e float in un computer, e come vengono trattati da compilatore e CPU, e quindi ragionare un po' sulle approssimazioni di calcolo... ma non è questa la sede per approfondire questi dettagli, non voglio annoiare nessuno, quindi vi rimando a una delle tante descrizioni che si trovano in rete, ad esempio questa.
Allora: abbiate on non abbiate letto la documentazione consigliata sopra, fidatevi: il problema esiste e, come anticipato più sopra, non è affatto una novità: per esempio il grande Donald Knuth (che cito spesso su queste pagine) ne parlava già nel suo mitico libro The art of computer programming (Volume II, 1981), dove proponeva alcune interessanti soluzioni del problema. E allora, facendo tesoro delle soluzioni proposte da Knuth, ho scritto questo semplice codice che vi invito a compilare ed eseguire:
#include <stdio.h>#include <math.h>#include <stdbool.h>// prototipi localibool isEqual(double a, double b, double myepsilon);// main() - funzione mainint main(){// a e b dovrebbero valere entrambi 1.3...double a = (0.3 * 3.0) + 0.4;double b = 1.3;// ...e invece...printf("a = %.32f\n", a);printf("b = %.32f\n", b);// ...ecco la prova del disastro!if (a == b)printf("i numeri a e b sono uguali!\n");elseprintf("i numeri a e b NON sono uguali...\n");// ma c'è una soluzione!if (isEqual(a, b, 0.001))printf("ma con la soluzione i numeri a e b sono uguali!\n");elseprintf("anche con la soluzione i numeri a e b NON sono uguali...\n");return 0;}// isEqual() - funzione di comparazione per variabili double (ispirata dalle// soluzioni di D.Knuth in "The art of computer programming" (vol.2, 1981))bool isEqual(double a, // primo double da compararedouble b, // secondo double da compararedouble myepsilon) // un numero piccolo per riferimento di comparazione{if (islessequal(fabs(a - b),myepsilon * (isgreater(fabs(a), fabs(b)) ? fabs(a) : fabs(b)))) {// se sono (approssimativamente) uguali ritorna truereturn true;}// se sono (approssimativamente) diversi ritorna falsereturn false;}
Dopo l'esecuzione vedrete questo:
a = 1.29999999999999982236431605997495b = 1.30000000000000004440892098500626i numeri a e b NON sono uguali...ma con la soluzione i numeri a e b sono uguali!
Ecco, la funzione isEqual() è un buon compromesso tra affidabilità e semplicità per eseguire comparazione tra double (e si può `facilmente scriverne una versione per i float). In realtà, il problema è più complesso di quello che sembra, e le soluzioni possibili sono molteplici, ma quella che vi ho appena proposto è interessante e flessibile (anche se alcuni puristi dicono che non è sufficiente).
Analizziamo il codice: per eseguire il confronto si usa un numero molto piccolo di riferimento che ho chiamato myepsilon per distinguerlo da eventuali epsilon definiti in alcuni linguaggi e compilatori come "numero più piccolo trattabile" (ad esempio nel C++ c'è: std::numeric_limits<T>::epsilon): in realtà non abbiamo bisogno di un epsilon assoluto ma possiamo usarne uno definito da noi (da qui il nome myepsilon) in base alla precisione che ci serve (e per questo ho definito questa soluzione come flessibile).
Notare che myepsilon non viene usato come comparatore assoluto ma come moltiplicatore, così la isEqual() adatta il suo comportamento ai numeri in uso (che potrebbero anche essere molto piccoli, più piccoli dello stesso myepsilon).
Avrete visto, poi, che non ho usato direttamente gli operatori ">" e "<=" per effettuare due comparazioni, ma ho invece usato due macro, fornite dalla glibc, che sono state scritte ad-hoc per eseguire queste operazioni con variabili floating-point: e visto che le abbiamo a disposizione usiamole! Sono sicuramente più affidabili degli operatori quando si eseguono operazioni in virgola mobile (anche perché tengono conto di eventuali operandi di tipo NaN). La lista di equivalenza che ci interessa è questa:
operatore macro corrispondente-----------------------------------------> int isgreater(x, y);>= int isgreaterequal(x, y);< int isless(x, y);<= int islessequal(x, y);!= int islessgreater(x, y);
E vi ricordo che, nel C++, le stesse macro hanno comportamento ed uso identici ma ritornano bool invece di int.
Comunque, dopo tutta questa sbrodolata su come fare e non fare le operazioni floating point, vi aggiungo un "consiglio da amico" che resetta il tutto (anzi: se avessi cominciato l'articolo così vi avrei risparmiato tutto il tempo della lettura). Tenetevi forte: la maniera migliore di fare le operazioni in virgola mobile è non farle proprio!
E mi spiego meglio: tornando con i piedi per terra e parlando di casi pratici, ad esempio la scrittura di Software industriale (un classico del C e C++) è sempre una buona idea trattare i dati (chessoio: misure di sensori: tensione, corrente, pressione e chi più ne ha più ne metta) come interi associati a una precisione: quindi il nostro Sottware conterrà un parametro di configurazione che è la precisione, che verrà usata come divisore solo quando si dovranno presentare i dati su un monitor o si dovranno stampare dei report. Ad esempio: supponendo di avere una precisione 1000 (che corrisponde a 3 decimali), una tensione di 1,525V per noi sarà, internamente, un intero che vale 1525, che poi diventerà 1,525 dividendolo per la precisione.
Semplice, no? Eseguendo internamente tutti i calcoli usando solo interi si eliminano automaticamente i problemi descritti in questo articolo. Evidentemente ci sono casi in cui usare direttamente double e float è indispensabile, ma nel Software industriale il trucco qui sopra è abbastanza usuale, ve l'assicuro.
Beh, per oggi può bastare. Abbiamo asfaltato un altro argomento dubbioso e prometto che ritorneremo (nel prossimo articolo) con l'argomento che avevamo lasciato pendente, quello delle nuove keyword del C (salvo ripensamenti dell'ultima ora, ah, ah, ah).
Ciao, e al prossimo post!
lunedì 22 novembre 2021
Gran Alignment: alignas e alignof come usare le nuove keyword del C - pt.2
Padre Janovich: Cosa posso fare per lei, Mister Kowalski?
Walt Kowalski: Sono venuto a confessarmi.
Padre Janovich: O Dio del Cielo, che cosa ha fatto?
Walt Kowalski: Niente, veda di non scaldarsi.
Padre Janovich: Che cosa sta per fare?
Walt Kowalski: Me la fa questa confessione o no?
Dopo aver celebrato l'ultima fatica del grande Clint Eastwood (Cry Macho), rimarremo sullo stesso tema e andremo indietro di qualche anno per celebrare un altro gran film: Gran Torino. Qui Clint è Walt Kowalski: un vecchio brontolone con un cuore d'oro. Un tipo preciso e rigoroso con una gran passione per la sua splendida "Ford Gran Torino" (e guai a toccargliela).
Ecco, per spostarci sul nostro amato C possiamo dire che anche usare due delle nuove keyword del C11, _Alignas e _Alignof, è indice di grande precisione e rigore in programmazione, anche se in realtà (e, forse, anche per fortuna) l'uso è abbastanza limitato ad alcuni casi particolari (ad esempio la programmazione embedded super-ottimizzata). Comunque, anche se di uso limitato sono keyword utili e, come minimo, bisogna sapere cosa sono e a che servono, perché non si sa mai...
![]() |
| ...ho appena rifatto l’allineamento delle ruote. Stai alla larga!... |
Allora, vediamo un po'. _Alignas e _Alignof sono due (relativamente) nuove keyword che tratteremo in coppia: questa è un po' una forzatura considerando che fanno cose abbastanza diverse, ma, visto che operano sullo stesso argomento (l'allineamento), permettetemi la forzatura (grazie). Ok andiamo, cominciamo con _Alignas:
Il type specifier _Alignas
_Alignas è un type specifier (come _Complex e _Imaginary, visti nella parte 1 dell'articolo) che si può applicare a tipi ed espressioni. Anche in questo caso è valida la norma già citata del nome che comincia con undescore+maiuscola per facilitare la retro-compatibilità (come già spiegato nell'articolo precedente e che vi invito a rileggere, please). E quindi, anche _Alignas deve essere, opportunamente, usato attraverso il suo header stdalign.h, che gli permette di trasformarsi, magicamente, nel nome alignas (che è molto più user-friendly, no?). E a cosa serve alignas? Ebbene, come si intuisce dal nome, serve ad allineare. Ma allineare cosa? Beh, per non perderci in cervellotiche spiegazioni farò direttamente un bell'esempio col codice, che, spero, renderà bene l'idea:
se compilate ed eseguite questo codice, il risultato (con GCC su Linux su amd64) sarà:sizeof char : 1sizeof short: 2sizeof float: 4il size di struct_a è: 12il size di struct_b è: 8
Che sorpresa! Vi dirò, questa non dovrebbe essere una sorpresa ma, mio malgrado, ho scoperto che non pochi programmatori (specialmente quelli non avvezzi alla programmazione low-level) si sorprendono. Allora, senza tentare di spiegare come funzionano la ruota e l'acqua calda, riassumerò (molto brevemente e in maniera semplificata) come funziona l'allineamento nel C (e chi lo sa già può, ovviamente, saltare a piè pari le prossime righe):
- La dimensione di una struttura è condizionata dall'allineamento dei dati usato dal compilatore (a sua volta condizionato dalla macchina: per questo nel test qui sopra ho specificato compilatore, OS e CPU): l'allineamento è sempre una potenza del 2, e l'elemento più grande di una struttura (nel nostro caso il float con size 4) condiziona l'allineamento di tutta la struttura.
- Visto il punto 1, si può affermare che la dimensione minima della nostra struttura è il multiplo di 4 più vicino alla somma dei membri (nel nostro caso la somma è 7, quindi nel caso ottimale abbiamo ottenuto 8).
- Il compilatore aggiunge, se necessario, dei byte di padding per raggiungere il size finale (nel caso di struct_b ha aggiunto un byte). Se mettiamo i membri a caso il padding può essere notevole (nel caso di struct_a abbiamo 5 byte di padding).
- Un buon ordine a prova di padding è quello in cui gli elementi adiacenti possono essere raggruppati in sottogruppi di (in questo caso) 4 byte: in struct_b pluto occupa un sottogruppo e paperino+pippo stanno in un altro sottogruppo, quindi il risultato è ottimizzato. Lo stesso non si può dir di struct_a dove pippo non può stare nello stesso sottogruppo di pluto (e quindi occuperà da solo 4 byte) e idem per pippo.
Evidentemente questo comportamento imprevisto delle strutture può comportare vari problemi, alcuni di efficienza (dati non ordinati e non ridotti al minimo non vengono ben digeriti dalla CPU) e altri problemi, più gravi, di comportamenti indesiderati: ad esempio, trasferendo dati binari tra macchine diverse è possibile che, scrivendo i dati in una struttura destinazione, non si possa accedere correttamente ai membri perché le due macchine hanno allineamenti diversi. Anticamente questo si risolveva aggiungendo dei membri dummy per allineare forzatamente la struttura (ed evitando il padding del compilatore), ma ora ci viene in aiuto alignas! (sospiro di sollievo).
E come si usa alignas? Ecco, vi propongo un esempio scemo (ma proprio scemo) che fa una cosa un po' controproducente ma rende bene l'idea:
E il risultato dell'esecuzione sarà:il size di struct_a è: 12il size di struct_b è: 8il size di struct_c è: 12
Ecco, grazie a questo codice abbiamo ottenuto di trasformare la nostra splendida e allineata struct_b in una struttura disallineata (o meglio: allineata come vogliamo noi): questo serve a poco, ma indica come con alignas possiamo modellare i dati (con un padding forzato) per avere un comportamento sicuro anche, ad esempio, nel trasferimento tra macchine diverse. Ovviamente si può fare un uso molto più intelligente e sofisticato di alignas, ma spero che l'esempio abbia chiarito le idee.
Ah, dimenticavo: alignas ha delle limitazioni d'uso (relative all'uso con variabili register e bit-fields, ecc.) ma per la lista completa vi rimando alle pagine del manuale. E adesso è il turno di _Alignof:
L'operatore _Alignof
_Alignof è un operatore, esattamente come sizeof con cui condivide il modo d'uso e una certa assonanza del nome. Anche _Alignof può essere, opportunamente, usato attraverso il suo header stdalign.h, che gli permette di trasformarsi (di nuovo magicamente) in alignof. Come si può intuire dal nome, alignof ritorna l'allineamento del tipo (o oggetto) passato in argomento, esattamente come sizeof ritorna il size. Quindi, grazie a questo nuovo operatore, possiamo controllare bene gli allineamenti degli oggetti che usiamo, e agire opportunamente in base alle necessità: risulta evidente che il campo applicativo è lo stesso di alignas (con cui, non a caso, condivide l'header), e un semplice esempio d'uso potrebbe essere il seguente:
struct struct_a {char pippo;float pluto;short paperino;};struct struct_b {float pluto;short paperino;char pippo;};struct struct_c {alignas(4) float pluto;alignas(4) short paperino;alignas(4) char pippo;};struct struct_d {alignas(8) float pluto;alignas(8) short paperino;alignas(8) char pippo;};int main(void){printf("il size di struct_a è: %zu\n", sizeof(struct struct_a));printf("il alignof di struct_a è: %zu\n", alignof(struct struct_a));printf("il size di struct_b è: %zu\n", sizeof(struct struct_b));printf("il alignof di struct_b è: %zu\n", alignof(struct struct_b));printf("il size di struct_c è: %zu\n", sizeof(struct struct_c));printf("il alignof di struct_c è: %zu\n", alignof(struct struct_c));printf("il size di struct_d è: %zu\n", sizeof(struct struct_d));printf("il alignof di struct_d è: %zu\n", alignof(struct struct_d));}
E il risultato dell'esecuzione sarà:
il size di struct_a è: 12il alignof di struct_a è: 4il size di struct_b è: 8il alignof di struct_b è: 4il size di struct_c è: 12il alignof di struct_c è: 4il size di struct_d è: 24il alignof di struct_d è: 8
Ossia: grazie ad alignof possiamo verificare le condizioni base di allineamento e possiamo anche verificare il risultato di una operazione di alignas. Semplice no? Anche in questo caso l'esempio era un po' scemo, ma spero che renda bene l'idea.
E con questo credo che sia tutto. Nel prossimo articolo tratteremo le restanti nuove keyword del C11, e per oggi può bastare: con tutti questi allineamenti e disallineamenti mi è venuto un mal di testa... e spero di non averlo fatto venire anche a voi!
Ciao, e al prossimo post!
