Le istruzioni SIMD, acronimo di Single Instruction Multiple Data, operano dentro milioni di computer, smartphone e server, eppure molti sviluppatori continuano a considerarle una materia riservata a chi scrive codec video, motori grafici o librerie matematiche. L’idea di base, in realtà, è molto più semplice: anziché elaborare un elemento per volta, il processore applica la stessa operazione a più valori raccolti in un unico registro.
Il modello SIMD risale ai primi studi sulle architetture vettoriali e parallele del secolo scorso. La sua diffusione nei processori general purpose è cresciuta con tecnologie come MMX e SSE su x86, seguite da AVX e AVX2; sul fronte Arm, Neon ha portato capacità analoghe su smartphone, dispositivi embedded, server e chip Apple Silicon.
Oggi un registro vettoriale può contenere 128, 256 o 512 bit, a seconda dell’architettura e delle estensioni disponibili. Significa, per esempio, che un’istruzione AVX2 può confrontare otto interi a 32 bit contemporaneamente, mentre AVX-512 può arrivare a sedici. Arm Neon lavora normalmente su vettori da 128 bit e può quindi gestire quattro valori a 32 bit per operazione. Non equivale sempre a ottenere una velocità otto o sedici volte superiore, ma il margine è abbastanza ampio da cambiare radicalmente le prestazioni di alcuni percorsi critici.
Mitchell Hashimoto, cofondatore di HashiCorp, ormai ex dell’azienda e oggi sviluppatore di Ghostty, ha in questi giorni portato l’attenzione sul tema SIMD con una tesi molto netta: ogni programmatore dovrebbe almeno saper riconoscere un caso adatto alla vettorizzazione. Non serve conoscere l’assembly né costruire algoritmi “estremi”: spesso basta imparare una struttura ricorrente composta da caricamento dei dati, operazione parallela, riduzione del risultato e gestione degli elementi avanzati.
SIMD non significa usare più core
Il primo aspetto da chiarire riguarda il tipo di parallelismo: SIMD non distribuisce il lavoro tra più core e non crea nuovi thread. L’esecuzione resta su un singolo core, ma una singola istruzione agisce su diverse porzioni di dati contenute in un registro vettoriale.
Immaginiamo di dover confrontare ogni byte di un buffer con un determinato valore. Un ciclo scalare carica un byte, esegue il confronto, aggiorna un indice e ripete la sequenza.
Con un vettore da 256 bit si possono caricare 32 byte in una volta sola e confrontarli simultaneamente. Il processore dovrà comunque eseguire altre istruzioni per leggere la memoria, produrre una maschera e interpretare il risultato; il confronto principale, però, procede su 32 elementi invece che su uno.
I singoli elementi di un vettore prendono il nome di lane. Un vettore da 256 bit può ospitare 32 lane da 8 bit, 16 lane da 16 bit, otto lane da 32 bit oppure quattro lane da 64 bit. La larghezza del registro non basta quindi a stabilire quanti valori verranno elaborati: conta anche il tipo di dato.
La vettorizzazione funziona bene quando molti elementi indipendenti richiedono la stessa operazione: scansione di stringhe, ricerca di delimitatori, conversione di colori, somme, conteggi, filtri, validazione di dati e trasformazioni numeriche sono candidati naturali. Un algoritmo ricco di dipendenze tra un’iterazione e la successiva, al contrario, può risultare difficile o persino impossibile da esprimere in forma vettoriale.
Ghostty mostra il vantaggio con un esempio concreto
Hashimoto parte da Ghostty, il suo emulatore di terminale, per mostrare che SIMD non è soltanto una tecnica utile nei laboratori o nei programmi scientifici. Può accelerare anche un’operazione apparentemente banale: controllare una lunga sequenza di caratteri e trovare il primo che richiede un trattamento speciale.
Un terminale riceve continuamente testo prodotto dai programmi. La maggior parte dei caratteri deve semplicemente comparire sullo schermo; altri, invece, rappresentano comandi di controllo. Possono indicare, per esempio, un ritorno a capo, uno spostamento del cursore o l’inizio di una sequenza che cambia colori e formattazione.
Ghostty deve quindi avanzare nel testo finché incontra uno di questi valori speciali: il metodo tradizionale controlla un carattere alla volta ovvero legge il primo, verifica se è normale, poi passa al secondo e così via.
Con SIMD il principio non cambia, ma il processore esamina più caratteri insieme. A seconda della CPU, Ghostty può controllarne quattro, otto o sedici con una sola operazione. Se sono tutti normali, il programma salta immediatamente al gruppo successivo: quando trova un blocco che contiene un carattere di controllo, individua la posizione esatta e riprende l’elaborazione da lì.
È questa l’idea importante trasferita da Hashimoto: SIMD non cambia il risultato dell’algoritmo; cambia il numero di dati che la CPU riesce a esaminare ad ogni passaggio.
Zig permette di descrivere l’operazione senza legarsi a una sola CPU
Ghostty è scritto in Zig: il linguaggio consente di dire al compilatore che un’operazione deve agire su un gruppo di valori, senza scrivere manualmente una versione completamente diversa per ogni famiglia di processori.
Il compilatore può quindi tradurre la descrizione nelle istruzioni più adatte alla macchina scelta: AVX2 o AVX-512 su determinati PC, Neon su Arm oppure un normale ciclo quando SIMD non è disponibile.
Ghostty segue proprio un approccio prudente: usa SIMD quando il compilatore e il processore lo consentono, altrimenti torna al metodo tradizionale. L’ottimizzazione migliora quindi le prestazioni senza diventare un requisito indispensabile per eseguire il programma.
Dove SIMD offre i vantaggi maggiori
SIMD dà il meglio quando il programma deve applicare la stessa operazione a una grande quantità di dati disposti in modo regolare: testi, immagini, audio, video e array numerici presentano spesso proprio queste caratteristiche.
Un programma di fotoritocco, per esempio, può modificare luminosità e contrasto di più pixel insieme. Un codec audio può elaborare diversi campioni durante ogni passaggio. Un motore di ricerca può confrontare blocchi di testo, mentre una libreria crittografica può applicare le stesse trasformazioni a numerosi byte.
Anche la gestione di formati comuni beneficia di questa tecnica: simdutf usa le istruzioni vettoriali per controllare e convertire testi UTF-8, UTF-16 e UTF-32. È una libreria adottata da progetti molto diffusi, tra cui Node.js, Chromium e WebKit.
simdjson applica lo stesso principio all’analisi dei documenti JSON: invece di leggere ogni carattere separatamente, esamina gruppi di byte per trovare parentesi, virgole, virgolette e altri simboli che definiscono la struttura del documento.
Non tutti gli sviluppatori devono costruire librerie tanto sofisticate. Il messaggio di Hashimoto è più semplice: quando un programma ripete milioni di volte la stessa operazione, vale la pena chiedersi se la CPU possa svolgere quel lavoro a gruppi.
Quando l’ottimizzazione non conviene
SIMD introduce comunque complessità. Per un ciclo che esamina pochi elementi, il lavoro può risultare inutile: anche un miglioramento del 50% ha poco valore se la funzione richiede soltanto una frazione irrilevante del tempo totale.
La regola più importante resta quindi la misurazione: prima si individua la parte realmente lenta del programma; poi si prova l’ottimizzazione e si confrontano i risultati. Senza un test credibile, è facile rendere il codice più difficile da mantenere senza ottenere benefici percepibili.
Lavorare a gruppi richiede anche una certa cautela nella lettura dei dati: se il programma vuole caricare otto elementi, deve verificare che ne siano rimasti almeno otto. Leggere oltre la fine di un buffer può causare errori (buffer overflow), arresti improvvisi o vulnerabilità di sicurezza.
Per questa ragione, molte implementazioni SIMD elaborano prima tutti i gruppi completi e lasciano gli ultimi elementi a un normale ciclo. È una soluzione semplice, facile da controllare e adatta alla maggior parte dei casi.
SIMD è meno misterioso di quanto sembri
Non serve imparare centinaia di istruzioni né scrivere direttamente in assembly per comprendere SIMD: basta partire dall’idea essenziale ovvero che una CPU può applicare lo stesso calcolo a più valori durante una sola operazione.
Gli algoritmi più adatti hanno alcune caratteristiche riconoscibili: elaborano molti dati, ripetono sempre lo stesso lavoro e non richiedono che ogni passaggio attenda il risultato del precedente.
La procedura pratica è altrettanto lineare: si prendono i dati a gruppi, si esegue l’operazione, si raccolgono i risultati e si gestiscono normalmente gli eventuali elementi rimasti.
SIMD non è una soluzione universale e non deve comparire in ogni ciclo. Ha senso soltanto nelle parti del programma che incidono davvero sulle prestazioni. Prima si misura, poi si verifica cosa riesce a fare il compilatore e infine, quando serve, si interviene in modo esplicito.
La riflessione di Hashimoto, alla quale vi invitiamo a fare riferimento per i dettagli tecnici, parte proprio da qui. Molti sviluppatori considerano la vettorizzazione un argomento troppo specialistico e la ignorano del tutto. In realtà, conoscere il principio permette almeno di riconoscere le situazioni nelle quali il software potrebbe diventare molto più prestazionale.