Multi core e programmazione concorrente

Bisogna adeguarsi al mondo che cambia…

Qualche giorno fa Donald Knuth, uno dei mostri sacri della programmazione, autore di vari libri della serie Art of Computer Programming, ha rilasciato un’intervista a InformIT in cui, tra le altre cose, esprime un parere estremamente negativo sulla attuale tendenza dei produttori hardware verso le architetture multi-core.

Il suo pensiero si può riassumere più o meno con: i produttori hardware non sanno più come migliorare le prestazioni dei loro chip, quindi hanno riciclato l’idea del multiprocessing, mantenendo le prestazioni fisse (o inferiori) su un singolo core, e mettendo diversi core per poter dire che la CPU è più veloce. In questo modo fanno ricadere la “colpa” nel mancato rispetto della legge di Moore (il raddoppio della potenza di elaborazione ogni 18 mesi) sui programmatori, dicendo che non sono in grado di scrivere software adatto alle nuove architetture.

Io, come diceva un saggio, sono “completamente d’accordo a metà col mister”.

Il problema hardware

Tutto si riconduce alla sostanziale stagnazione delle prestazioni delle CPU single-core da qualche anno a questa parte. Si sono raggiunte miniaturizzazioni oltre le quali si rischia di creare corto-circuiti tra i componenti, e frequenze oltre le quali si rischia di generare interferenze tra le piste o, peggio, surriscaldamenti e consumi sproporzionati, che possono portare la CPU a fondere o a esplodere letteralmente in mancanza di sistemi di raffreddamento che sono sempre più esagerati (dai ventoloni da 6″ a 5-6000 giri/minuto a sistemi di raffreddamento a liquido o a celle di Peltier).

Semplicemente sembra che la tecnologia abbia quasi raggiunto il suo limite e, a meno di un “breakthru” nei metodi costruttivi, che comunque molti stanno cercando nel campo ottico o quantistico, sarà difficile spremere ulteriore potenza ai chip.

La soluzione, quindi, è stata quella di riciclare la vecchia idea del multi-processore, ma ottimizzandola: si mettono più processori sullo stesso chip, in modo da non occupare il bus principale di sistema durante le sincronizzazioni tra core, che quindi rimane libero per i trasferimenti da e per la RAM e il resto del sistema. Sono ormai diffusissimi i dual core, e si vedono tri-core, quad-core e anche chip a 8 core.

Il risultato, indubbiamente, è quello di raddoppiare, triplicare, quadruplicare o ottuplicare la “potenza” disponibile.

Il problema software

Dal punto di vista software le cose non sono così semplici, purtroppo.

Il primo problema è quello a carico del Sistema Operativo, che deve garantire una certa sicurezza all’utente: che i suoi dati siano integri. In un’architettura multiprocessore (o multi-core, che è praticamente la stessa cosa) fisicamente vengono eseguite più operazioni contemporaneamente. Questo significa che ognuno dei core può (o potrebbe) modificare la stessa cella di memoria, o magari leggerla mentre un’altro la sta scrivendo, per non parlare dell’accesso contemporaneo a una periferica.

Parte di questo problema viene risolta dall’hardware, che arbitra l’accesso al bus ai processori, ma buona parte viene comunque delegata al S.O., visto che comunque una operazione che un software ritiene “elementare” come può essere scrivere un carattere sullo schermo viene spezzettata in decine o centinaia di istruzioni a livello assembly.

Inoltre il S.O. deve mantenere una certa affinità di un processo con una CPU, per evitare che dopo un context switch, un processo rimbalzi tra una CPU e l’altra, svalidando continuamente la cache relativa, e quindi ammazzando le prestazioni. Ma anche l’affinità deve essere tenuta sotto controllo, per evitare che, morti tutti i processi che giravano su una CPU, i restanti sfruttino solo l’altra, dimezzando le prestazioni.

Questo porta quindi a dei rallentamenti per proteggere zone di memoria o semplicemente per decidere su quale CPU/core debba essere eseguita una certa operazione.

L’altra faccia della medaglia è rappresentata dagli algoritmi: se c’è un solo processo che gira (e che fa calcoli pesanti, per esempio), questo occuperà una sola CPU, mentre l’altra sarà completamente inerte, dimezzando le prestazioni nel caso di un dual-core, o riducendole a 1/8 nel caso di un 8-core. Nel caso di 64 core…

Alcuni algoritmi possono essere resi paralleli, ma anche in questo caso si presentano diversi problemi: per esempio l’algoritmo potrebbe essere parallelizzabile in due “stream” di calcolo, ma allora in una CPU ad 8 core ne sfrutterebbe solo due. Oppure potrebbe essere parallelizzabil, per esempio, con un numero di processi pari a 1/4 del totale dei dati. Ma allora con 16 “dati” si potrebbero sfruttare bene 4 core ma non sfruttarne 8, ma soprattutto con un milione di “dati” si sfrutterebbero bene 250.000 core, ma avendone solo 2 si creerebbero 125.000 processi per core, costringendo il S.O. a gestire questa enorme coda di processi, con la possibilità di passare più tempo a sincronizzare i processi che ad eseguirli.

Visto l’attuale tendenza (2 core, 4 core, 8 core e in aumento costante) è difficile pensare ad algoritmi “ben parallelizzabili” su diverse architetture. In pratica è un bersaglio mobile.

La “terza faccia della medaglia” è data dal fatto che non è affatto semplice, oggi come oggi, scrivere codice parallelo.

Si possono scrivere programmi multiprocesso per isolare i dati da elaborare e comunicare solo tramite dei “bus” (code messaggi, buffer, ecc.) tra i vari processi in esecuzione, oppure multithread, condividendo la stessa area di memoria per evitare l’overhead dei suddetti bus, ma col rischio di andare a sovrascrivere aree di memoria sbagliate e quindi mandando a monte tutti i calcoli effettuati.

I linguaggi di programmazione attuali raramente offrono facilitazioni da questo punto di vista, quindi si tratta di un lavoro fondamentalmente manuale di sincronizzazione e trasferimento dati. Operazioni che rischiano di azzerare i vantaggi di un multicore, se effettuate in modo meno che ottimo.

Conclusioni

La situazione è abbastanza complessa, ma è già stata affrontata in passato, ed esistono tecniche di programmazione e librerie ben collaudate, ma rimane lo scoglio della difficoltà di ottimizzazione, soprattutto rispetto ad architetture che cambiano continuamente numero di core.

Dall’altra parte il multicore è un “male necessario”, altrimenti saremmo ancora fermi a 2 anni fa con le prestazioni, ed il nuovo processore Atom di Intel ne è la dimostrazione: single core a 1.6 GHz più lento del Celeron M a 900 MHz. Certo, consuma meno, ma fornisce prestazioni maggiori solo se in modalità dual-core.

Dovremo quindi abituarci a convivere con questa architettura almeno per qualche anno (poi, per i soliti corsi e ricorsi storici, si tornerà al single core, magari quantistico, finché non si riterrà nuovamente necessario ricorrere ai multi-core quantistici, ecc. ecc.).

Per i programmatori significa adeguarsi ad imparare la programmazione concorrente, e la mia speranza è che i nuovi linguaggi che si stanno affacciando ora sul mondo (D, Vala, ecc.) forniscano strumenti semplici per ottenerla. Con alcuni linguaggi semplicemente è impossibile programmare in modo concorrente, quindi potrebbero essere destinati se non all’oblio almeno a un forte ridimensionamento, in caso non si adeguassero.

Per gli utenti significa capire il proprio PC, e sfruttarlo al meglio. Per esempio lanciare due istanze della stessa applicazione contemporaneamente al lavoro su dati diversi per sfruttare entrambi i core, se l’applicazione non li supporta di suo. Un caso emblematico: compilando software su Linux con make è sufficiente specificare l’opzione -j seguita dal numero di job contemporanei da eseguire. Conviene usare n+1, dove n è il numero di CPU/core, quindi 3 per un dual-core, 5 per un quad-core, ecc. (il +1 serve a sfruttare tutte le CPU in compilazione mentre il processo extra legge o scrive su disco).