Abbiamo parlato di calcolo distribuito ovvero combinare la potenza di calcolo di più macchine, per raggiungere uno scopo. Il calcolo parallelo invece riguarda la stessa macchina, dato che di default un linguaggio di programmazione (C, Python o altro che sia) lavora su un singolo core della CPU (o meglio, osservando bene tramite un tool come Htop, si nota che un thread viene sfruttato al massimo mentre un secondo thread in modo scarsamente efficiente).
Ad esempio un processore Dual Core con 4 thread, clock 2.6 GHz multi-core e clock di picco single-core 3.3 GHz, ha risolto l'esempio Calcolo lista numeri primi fino ad N: algoritmo più efficiente in C in 7,1 secondi mentre inserendo un semplicissimo comando vengono sfruttati tutti i thread e il tempo totale diventa 11,1/4=2,77 secondi. Ho evidenziato 11,4 ovvero il valore mostrato, dato che è quanto viene stampato a video, il valore corretto però va suddiviso per il numero di thread (si può fare benissimo una verifica manuale che lo conferma!). Quindi con 4 thread, la velocità non possiamo dire che quadruplica, ma quasi, aumenta notevolmente!
Calcolo parallelo in C: come implementarlo
Veniamo al sodo, quello che ci interessa 😀 Esistono varie procedure, alcune piuttosto complicate, invece vediamo quello che fanno le OpenMP API (approfondimento su learn.microsoft.com).
La procedura più semplice, veramente semplicissima da implementare e molto efficace è questa:
#pragma omp parallel
{
/* blocco di codice */
}
Quindi il blocco di codice racchiuso fra parentesi graffe, esegue quanto indicato dalla direttiva precedente. Se abbiamo un ciclo con molte iterazioni, dobbiamo semplicemente creare questa struttura, questo blocco, che racchiuda il nostro ciclo e questo verrà eseguito in parallelo. Fantastico! Per compilare da terminale è sufficiente introdurre questa modifica (parametro "fopenmp"): gcc file.c -o file -fopenmp
(nota: in futuro potrebbe essere deprecato questo comando e suggeriti comandi alternativi, concettualmente non cambia, approfondimento).
Questo è ciò che consiglio e più semplice da implementare. Le direttive OpenMP consentono comunque personalizzazioni aggiuntive, a seconda delle necessità, per rendere l'esecuzione del codice ancora più efficiente: lasciato così immaginiamo una gestione al 90% delle potenzialità, fa tutto in automatico. Per esigenze specifiche possiamo ad esempio destinare singolarmente le operazioni ai vari thread, con il comando #pragma omp single
. Può avere senso quando a abbiamo a priori stabilito noi una corretta suddivisione del codice, che sia più efficiente rispetto ad un funzionamento automatico. Oppure se sappiamo di voler destinare altri thread a compiti specifici e quindi abbiamo interesse a distribuire il carico in questo modo. Sono esigenze comunque specifiche e avanzate, la guida di learn.microsoft.com approfondisce il tema. Così come compilando da terminale tramite gcc, è sufficiente il comando scritto prima, se invece vogliamo ulteriori funzionalità di gestione più avanzata, possiamo includere la libreria #include <omp.h>
.
Ora una riflessione finale: viste le potenzialità del calcolo parallelo (sfruttare al 100% la macchina) e del calcolo distribuito (sfruttare più macchine che lavorano allo stesso obiettivo, programma) pensiamo alle enorme potenzialità computazionali date dall'unione di questi due!