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.

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?

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 main
int 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");
else
printf("i numeri a e b NON sono uguali...\n");

return 0;
}

ecco, questo semplicissimo codice stampa questo:

a = 1.29999999999999982236431605997495
b = 1.30000000000000004440892098500626
i 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 locali
bool isEqual(double a, double b, double myepsilon);

// main() - funzione main
int 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");
else
printf("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");
else
printf("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 comparare
double b, // secondo double da comparare
double 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 true
return true;
}

// se sono (approssimativamente) diversi ritorna false
return false;
}

Dopo l'esecuzione vedrete questo:

a = 1.29999999999999982236431605997495
b = 1.30000000000000004440892098500626
i 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:

#include <stdio.h>

struct struct_a {
char pippo;
float pluto;
short paperino;
};

struct struct_b {
float pluto;
short paperino;
char pippo;
};

int main(void)
{
printf("sizeof char : %zu\n", sizeof(char));
printf("sizeof short: %zu\n", sizeof(short));
printf("sizeof float: %zu\n", sizeof(float));

printf("il size di struct_a è: %zu\n", sizeof(struct struct_a));
printf("il size di struct_b è: %zu\n", sizeof(struct struct_b));
}
se compilate ed eseguite questo codice, il risultato (con GCC su Linux su amd64) sarà:
sizeof char : 1
sizeof short: 2
sizeof float: 4
il size di struct_a è: 12
il 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):

  1. 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.
  2. 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).
  3. 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).
  4. 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:

#include <stdio.h>
#include <stdalign.h>

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;
};

int main(void)
{
printf("il size di struct_a è: %zu\n", sizeof(struct struct_a));
printf("il size di struct_b è: %zu\n", sizeof(struct struct_b));
printf("il size di struct_c è: %zu\n", sizeof(struct struct_c));
}
E il risultato dell'esecuzione sarà:
il size di struct_a è: 12
il size di struct_b è: 8
il 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 è: 12
il alignof di struct_a è: 4
il size di struct_b è: 8
il alignof di struct_b è: 4
il size di struct_c è: 12
il alignof di struct_c è: 4
il size di struct_d è: 24
il 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!

venerdì 22 ottobre 2021

Cry Keyword: complex
come usare le nuove keyword del C - pt.1

Michael "Mike" Milo: [riferendosi ai federali che lo hanno appena lasciato andare in cambio di una mazzetta] Se avessero un cervello, sarebbero pericolosi...

Celebriamo oggi l'ultimo film che ci ha regalato un mito vivente del Cinema con la C maiuscola, Clint Eastwood. Cry Macho è un road-movie "crepuscolare" in cui il nostro Clint interpreta un vecchio cow-boy impegnato in un viaggio, malinconico e gioioso allo stesso tempo, che parte dal sud degli US e si prolunga nel Messico. E questo ci può ispirare a iniziare anche noi un viaggio alla scoperta delle ultime novità del C, quelle che pochi usano ma sono, come sempre, molto utili: nel C si aggiungono poche cose ma succose (ad esempio complex, di cui parleremo tra poco).

...ah, se esistessero i numeri immaginari. Peccato che sono solo frutto della mia immaginazione...

E comincerò con una auto-citazione dal mio ultimo articolo (dopodiché mi auto-denuncerò per plagio):

...un po’ come è successo, per esempio, con le variabili booleane _Bool definite nell’header stdbool.h: questa norma del nome che inizia con undescore+maiuscola è seguita per tutte le nuove keyword del C, che possono, poi, essere usate con l’header corrispondente per “raddrizzare” il nome...

Ecco, credo che, per mantenere la linea con gli ultimi articoli sia venuto il momento di parlare delle nuove (vabbè, risalgono al C99 e al C11, diciamo allora "relativamente nuove") keyword del C, quelle che sono state introdotte un po' per volta, col contagocce (e giustamente! Perché il C è un linguaggio solido, stabile e senza fronzoli). E visto che sono arrivate quando il linguaggio era già da qualche annetto che veniva usato in lungo e in largo per il mondo (è da sempre uno dei linguaggi più usati) bisognava avere un occhio di riguardo per il codice già scritto con implementazioni private delle keyword non ancora standardizzate.

E bool  è il classico esempio: supponendo che qualcuno (in molti, probabilmente) abbia scritto codice con un tipo booleano user-defined e lo abbia chiamato "bool" (molto originale, no?), l'introduzione nel linguaggio di un nuovo tipo con lo stesso nome (e quindi riconosciuto da compilatore) avrebbe creato qualche conflitto con il codice pre-esistente, quindi il committee ISO ha pensato bene che tutte le nuove keyword introdotte si sarebbero chiamate con nomi che iniziano con undescore+maiuscola, cominciando con _Bool che credo sia stato il primo esempio (è stato introdotto nel C99).

Come funziona questo? È semplicissimo: se voglio usare il nuovo tipo booleano scrivo:

#include <stdbool.h>
...
bool mybool_a = true;
bool mybool_b = false;
...

Evidentemente dentro l'header standard stdbool.h  vengono definite, tra le altre cose, le macro true (che vale 1), false (che vale 0) e bool  (che corrisponde al nuovo tipo standard _Bool). E invece, se ho una mia vecchia implementazione di bool e voglio continuare a usarla basta non includere stdbool.h  e amici come prima.

E quali sono le "relativamente nuove" keyword del C? Eccole!

C99:
_Bool
_Complex
_Imaginary

C11:
_Alignas
_Alignof
_Atomic
_Generic
_Noreturn
_Static_assert
_Thread_local

di alcune ho già parlato recentemente (_Atomic nello scorso articolo e _Bool proprio ora, non so se vene siete accorti) o anticamente (_Thread_local, usando però __thread, che è una estensione del GCC sostanzialmente identica). In questo articolo direi di soffermarci sulle due che ci mancano del C99, ovvero _Complex e _Imaginary.

_Complex e _Imaginary, come si può ben intuire dai nomi, sono due nuovi type specifier  (come char, int, ecc.) che permettono l'uso dei numeri complessi nel C. Nessuno storca il naso ("ma chi ha bisogno dei numeri complessi?"): non sono di uso molto comune, ma non guasta poterli usare, e, se non vi servono, pensate che chi li usa (e potrebbero essere più di quelli che pensate) vi manderà maledizioni se dite che se ne può fare a meno (quell'irascibile di mio cuggino ad esempio li usa spesso). Ovviamente non è questa la sede per descrivere cosa sono e a cosa servono i numeri complessi, visto che ci sono milioni di fonti che li descrivono molto meglio di quanto potrei fare io.

Queste due nuove keyword viaggiano in coppia, e si possono utilizzare, entrambe, attraverso l'header complex.h. Un solo header per due nuove keyword? Ebbene si, sono troppo collegate per usarne due diversi, infatti _Imaginary serve a definire la parte immaginaria di un numero complesso, quindi è giustificato che tutto sia definito in complex.h. E come si usano? Prima di tutto bisogna evidenziare che, per la loro natura (matematica) stessa, si accoppiano a tipi float, e che essendo type specifier c'è una certa libertà di scrittura, per cui possiamo scrivere questo:

...
...
// questo formato è Ok
_Complex float cf1;
_Complex double cd1;
_Complex long double cld1;

// ma anche questo è Ok
float _Complex cf2;
double _Complex cd2;
long double _Complex cld2;
...

e, usando le macro definite nell'apposito header complex.h, possiamo "abbellire" il codice in:

#include <complex.h>
...
// questo formato è Ok
complex float cf1;
complex double cd1;
complex long double cld1;

// ma anche questo è Ok
float complex cf2;
double complex cd2;
long double complex cld2;
...

E adesso è venuto il momento di un bel caso pratico di uso di _Complex, che è, spero, più chiaro di mille spiegazioni. Facciamo cantare il codice!

#include <stdio.h>
#include <complex.h>

int main(void)
{
// dichiaro un numero complesso
complex float cf = 5 + 3 * I;

// eseguo due operazioni aritmetiche
complex float mult_cf = cf * 2;
complex float div_cf = mult_cf / 2;

// stampo i risultati
printf("(5.0 + 3.0i) * 2 = %.1f %+.1fi\n",
creal(mult_cf), cimag(mult_cf));
printf("(%.1f %+.1fi) / 2 = %.1f %+.1fi\n",
creal(mult_cf), cimag(mult_cf), creal(div_cf), cimag(div_cf));
}

credo che l'esempio sia abbastanza chiaro: dichiariamo un numero complesso, lo moltiplichiamo per 2 e dividiamo il risultato per 2, per ottenere lo stesso valore di partenza (tanto per dimostrare la correttezza delle operazioni). Se compilate ed eseguite otterrete:

(5.0 + 3.0i) * 2 = 10.0 +6.0i
(10.0 +6.0i) / 2 = 5.0 +3.0i

Funziona! Notare, nel codice, la presenza della macro I (definita, ovviamente, in complex.h) che serve a definire la parte immaginaria del numero complesso (si usa come moltiplicatore per trasformare un "numero reale" in "numero immaginario").

Notare anche che per stampare i valori ho usato delle funzioni di libreria i cui prototipi sono dichiarati in complex.h: creal() e cimag() che, come si intuisce dai nomi, servono a estrarre, rispettivamente, le parti reale e immaginarie di un numero complesso, che poi possono essere agevolmente stampate con una printf(), come nell'esempio. Nel nostro header sono dichiarate molte altre di queste funzioni per numeri complessi, con nomi facilmente riconducibili alle equivalenti funzioni per "numeri normali". Abbiamo, ad esempio, cabs() per il valore assoluto, clog() per il logaritmo naturale, ccos() per il coseno, e chi più ne ha più ne metta.

Ok, per oggi può bastare. Nei prossimi articoli parleremo delle altre keyword della lista mostrata all'inizio dell'articolo. E mi raccomando: non trattenete il respiro nell'attesa!

Ciao, e al prossimo post!

venerdì 17 settembre 2021

Atomic: Endgame
come, quando e perché usare il type qualifier _Atomic in C

Scott Lang/Ant-Man: È una pazzia!
Natasha Romanoff/Vedova Nera: Scott, io ricevo e-mail da un procione, perciò nulla sembra una pazzia ormai.

E siamo arrivati al terzo capitolo della mini-saga degli Endgame sui type qualifier del C. Questa volta tocca a _Atomic, che, rispetto ai qualificatori trattati nei due precedenti articoli (su volatile e restrict), ha un ruolo più importante nel linguaggio. E, visto che nel nuovo mondo post Infinity War la nostra Natasha non si sorprende di parlare con un procione, noi non siamo affatto sorpresi di avere (finalmente!) la possibilità di usare variabili atomiche nel C.

...con le variabili atomiche la mia vita è cambiata da così a così...

Anche stavolta (per la solita pigrizia) seguirò la stessa impostazione dei due articoli precedenti ("Squadra che vince non si cambia") quindi questo è il momento delle notizie, e cominceremo con la buona:

  • La buona notizia (che ho già spoilerato sopra) è che _Atomic è un qualificatore veramente molto utile, che ci può rendere la vita più facile nella scrittura del codice (solo nella programmazione multithread, eh!). _Atomic è arrivato con lo Standard C11 insieme ad altre poche, mirate e utili novità (che vi invito a esaminare e ad usare). _Atomic ha un nome, ahimè, un po' strano che è stato scelto per curare la retrocompatibilità, visto che è possibile che ci siano delle implementazioni "private" e pre-C11 delle variabili atomiche (l'attesa è stata lunga!); un po' come è successo, per esempio, con le variabili booleane _Bool definite nell'header stdbool.h: questa norma del nome che inizia con undescore+maiuscola è seguita per tutte le nuove keyword del C, che possono, poi, essere usate con l'header corrispondente per "raddrizzare" il nome (la stabilità e la retrocompatibilità sopra tutto!).

(...e visto che siamo in argomento: come ben sapete le periodiche revisioni dello Standard del C si limitano sempre a poche e essenziali aggiunte, già che il C è un linguaggio, per sua natura ed origine, stabile e completo. Proprio il contrario del C++ che, ad ogni revisione, sembra dire: "Fino ad oggi abbiamo scherzato, ma da adesso il C++ è un linguaggio nuovo.". Ma non è che avere un linguaggio nuovo ogni tre (3!) anni è un po' eccessivo? E non è che aggiungere n-mila novità ogni volta (molte di uso dubbioso e che, probabilmente, quasi nessuno userà mai, come già fatto notare dal Maestro Rob Pike) è un po' ridicolo e offensivo per chi già lo usa bene (tra cui, modestamente, il sottoscritto) da anni? Ma questa è un altra storia...)

E ora è il turno della cattiva notizia, che è meno cattiva di quel che sembra:

  • _Atomic ha una doppia personalità: è un type qualifier ma è anche un type specifier, e questo origina, come vedremo tra poco, alcune considerazioni e dubbi.

E veniamo al dunque: nel titolo di questo articolo preannunciavo la descrizione del come, quando e perché usare _Atomic in C, per cui ora ci tocca cominciare con:

IL COME:

Come abbiamo appena visto _Atomic ha due personalità, e quindi possiamo scrivere cose come queste:

int dummy; // un int normalissimo che si chiama dummy
_Atomic int ato_dummy1; // uso come type qualifier: ato_dummy1 è una
// variabile che è una versione atomica di un int
_Atomic(int) ato_dummy2; // uso come type specifier: ato_dummy2 è una
// variabile di un nuovo tipo "int atomico"

La differenza tra qualificatore e specificatore è sottile: ad esempio nel semplice caso descritto sopra ato_dummy1  e ato_dummy2  sono, in effetti, la stessa cosa. Diciamo che la versione "qualifier"  ha un uso più familiare (specialmente dopo aver letto gli ultimi articoli... li avete letti, vero?) e può essere anche combinata con gli altri type qualifier (volatile, restrict e const). Invece la versione "specifier" non può essere combinata con i qualificatori  (incluso lo stesso _Atomic). Bisogna anche aggiungere che (come specificato nello standard) un "tipo atomico" non necessariamente ha la stessa struttura di basso livello (size, allineamento, ecc.) del tipo da cui deriva (però potrebbe averla: dipende dall'implementazione).

E adesso un piccolo esempio di casi reali:

_Atomic const int *ptr1; // pointer a un atomic const int
const _Atomic(int) *ptr2; // anche questo è un pointer a un
// atomic const int

_Atomic const volatile int *ptr3; // pointer a un atomic const volatile int
const _Atomic(volatile int) *ptr4; // errore: non si può combinare se si usa
// come type specifier

E per concludere in bellezza questo paragrafo, vi dirò un segreto di pulcinella: anche se esistono le due possibilità di uso viste sopra, in pratica non se ne usa nessuna delle due, visto che, molto convenientemente, l'uso delle variabili atomiche passa attraverso l'inclusione dell'header  stdatomic.h  che contiene un gran gruppo di tipi predefiniti (sono ben 37), che si dovrebbero usare in maniera preferente. Quindi, ad esempio, un codice normale è questo:

#include <stdatomic.h> // include standard del type qualifier _Atomic

// funzione main
int main(void)
{
// dichiaro due variabili atomiche usando i tipi predefiniti in stdatomic.h
atomic_bool my_ato_bool;
atomic_int my_ato_int;

// uso le variabili atomiche my_ato_bool e my_ato_int
...
}

E direi che a questo punto è evidente che perdersi nel dilemma "uso il qualificatore o lo specificatore?"  invece di usare i tipi predefiniti e un po' una sega mentale, tranne in particolari casi in cui si vogliono usare versioni atomiche di tipi complessi (strutture) invece che di tipi semplici (e anche questo si puo fare!).

E ora siamo pronti a passare al prossimo punto:

IL QUANDO:

Come anticipato sopra, _Atomic è decisamente riservato all'uso nella programmazione multithread: infatti, in questo tipo di programmazione l'accesso ai dati condivisi (shared, per gli amici) deve essere sempre "sicuro", perché se due (o più) thread accedono simultaneamente allo stesso dato si può produrre il famigerato "data race" con effetti indesiderati e imprevedibili. Al proposito lo standard del C dice:

"The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior." [The C Standard, section 5.1.2.4, paragraph 25 [ISO/IEC 9899:2011]]

Nel mondo pre-C11 queste situazioni si risolvevano, al solito, usando i classici strumenti di sincronizzazione dei thread (ad esempio i mutex, oppure usando, molto erroneamente, volatile), ma ora, grazie a _Atomic è molto più semplice scrivere codice senza "data races", usando variabili atomiche per i dati condivisi. Gli strumenti di sincronizzazione tipo mutex e spinlock si usano ancora, ma per usi più specifici di sincronizzazione e non di accesso a dati shared.

Quale è, ora, il prossimo punto? Ah si, è:

IL PERCHÉ:

Il perché direi che si può descrivere facilmente: come visto nel punto precedente, nella programmazione multithread è obbligatorio evitare i "data races". Questo si è sempre ottenuto usando gli strumenti classici a disposizione (mutex, spinlock, ecc.). Ora, con l'introduzione in C11 di _Atomic ci viene data la possibilità di risolvere lo stesso problema in modo più semplice (ottenendo codice più compatto e pulito) ed efficiente (le operazioni atomiche non eseguono lock dei thread, e quindi sono generalmente più veloci delle operazioni fatte con i mutex). Quindi direi che usare _Atomic è una scelta azzeccatissima e quasi obbligata.

E adesso, come sempre, è venuto il momento del codice, per cui vi propongo un esempio semplicissimo, che è uno dei tanti possibili. Anche stavolta l'ho scopiazzato da cppreference.com, perché non avevo voglia di scriverne uno ad-hoc, ma anche perché è veramente un bell'esempio, molto calzante. E vi dirò di più: questo stesso esempio (identico o con piccoli cambi) l'ho trovato anche in altre pagine della rete, quindi non posso dire esattamente quale sia la fonte originale (che potrebbe non essere neppure cppreference.com, ah ah ah). Vai col codice!

#include <stdio.h>
#include <threads.h>
#include <stdatomic.h>

atomic_int ato_cnt; // un int atomico usato come contatore
int cnt; // un int normale usato come contatore

// updateCnt() - funzione di update dei counter eseguita da ogni thread
int updateCnt(void* thr_data)
{
// incremento i counter per 1000 volte
for (int n = 0; n < 1000; ++n) {
++ato_cnt;
++cnt;
// per questo esempio, il relaxed memory order è sufficiente,
// quindi si dovrebbe usare:
// atomic_fetch_add_explicit(&ato_cnt, 1, memory_order_relaxed),
// ma va bene anche il semplice incremento (++) usato
}

return 0;
}

// funzione main
int main(void)
{
// avvio 10 thread
thrd_t thr[10];
for (int n = 0; n < 10; ++n)
thrd_create(&thr[n], updateCnt, NULL);

// attendo la fine dei 10 thread
for(int n = 0; n < 10; ++n)
thrd_join(thr[n], NULL);

// i risultati!
printf("Il contatore atomico vale: %u\n", ato_cnt);
printf("Il contatore normale vale: %u\n", cnt);
}

E se compilate ed eseguite l'esempio i risultati saranno (si fa per dire) sorprendenti: ato_cnt  varrà 10000 mentre cnt  varrà molto meno (ma chissà perché?).

Notare, nella nota scritta dopo il ++cnt , che si cita la funzione atomic_fetch_add_explicit(): ecco, le operazioni atomiche possono essere semplici (ad esempio il "++" eseguito su una variabile atomica è garantito come "atomico"), ma in alcune situazioni bisogna usare alcune operazioni speciali (tipicamente di fetch, load e store) e, magari, bisogna anche curare il modo di riempimento della memoria (il memory_order). Ma questi sono dettagli che vi consiglio di approfondire autonomamente leggendo bene i manuali e scrivendo qualche programma di test.

E per oggi penso che possa bastare. Teoricamente dovrei dedicare un altro Endgame al type qualifier const... ma const non è proprio una novità, ed è, oramai, di uso molto comune (specialmente per chi usa anche il C++), quindi quasi quasi lo salto, perché non ci sono molti misteri da svelare sull'argomento. Vedremo...

Ciao, e al prossimo post!