Le CPU fotoniche hanno ancora parecchio da chiarire (e da farsi aspettare)

Di recente è apparsa una pubblicazione scientifica riguardo ciò che viene decantata come una rivoluzione nell’ambito della computazione: una CPU realizzata con componenti interamente ottici, la quale sulla carta dovrebbe non soltanto essere ben più efficiente, ma anche in grado di gestire i compiti tipici di quelle attuali (basate sull’elettronica) e, per questo, definita “general-purpose“.

Non avendo competenze in ambito ottico/elettronico mi limiterò ad analizzare alcuni punti per lo più in ambito architetturale, fornendo la mia opinione e/o esprimendo i miei dubbi su quanto riportato nel pezzo. Il quale pretenderebbe di far chiarezza su presunti luoghi comuni, ma il cui effetto è diametralmente opposto rispetto alle aspettative, come vedremo (riportandone le parti salienti).

I luoghi comuni sulle CPU CISC… in realtà non lo sono

Modern CISC (complex instruction set computer) architectures, such as x86, contain a multitude of instructions that are barely used, if at all, by most pieces of software, and were included to speed up very specific edge cases. The renewed interest in RISC (reduced instruction set computer) architectures is a clear indication, that this excess is not necessarily a good development. […] For general-purpose tasks on an x86, the most common instructions are MOV, ADD, PUSH and those discussed in Table II. But the availability of billions of transistors in electronic ICs has allowed the x86 instruction set to bloat to over 1000’s of instructions optimizing even the rarest of use.

Sfortunatamente per gli autori, la situazione è diametralmente opposta a quella riportata. Infatti i cosiddetti processori RISC hanno avuto vita brevissima, poiché gli avanzamenti tecnologici hanno, di fatto, portato alla demolizione dei principi sui quali erano fondati, come ampiamente dimostrato qui in una recente serie di articoli (Il definitivo RISC vs CISC). Al massimo si può parlare di architetture L/S (Load/Store), ma certamente non di RISC!

Difatti quelli che vengono ancora oggi spacciati come RISC tutto sono, fuorché architetture dotate di un insieme ridotto di istruzioni, e ciò essenzialmente per una motivazione: le prestazioni. Cosa che, peraltro, è banalmente verificabile andando a snocciolare l’elenco delle istruzioni di tali architetture.

Un esempio è RISC-V (citato una volta nell’articolo), che da tempo ha superato le 500 istruzioni tenendo conto di tutte le estensioni già ratificate (a cui se ne aggiungeranno altre man mano che ne verranno approvate altre). Sfido chiunque a continuare a parlare di RISC = “insieme ridotto di istruzioni” quando da parecchi anni processori che vengono spacciati come tali ne integrano centinaia…

E’ anche vero che x86 abbia tante istruzioni e diverse vengano utilizzate raramente, ma se sono lì è perché diventano utilissime e fanno la differenza quando vengono eseguite, come dimostra l’esempio (che non è certo l’unico) di RPCSX3 (il più famoso emulatore PS3 per PC) preso dal blog di uno sviluppatore:

The performance when targeting SSE2 is absolutely terrible, likely due to the lack of the pshufb instruction from SSSE3pshufb is invaluable for emulating the shufb instruction, and it’s also essential for byteswapping vectors, something that’s necessary since the PS3 is a big endian system, while x86 is little endian.

I numeri sono a dir poco impietosi: dalla versione di codice che usa le SSE2 a quella che usa le SSE4.1 (il cui contributo va quasi esclusivamente a una singola istruzione PSHUFB) il numero di fotogrammi generati (in quella scena, ma è un risultato complessivamente valido) passa da circa 5 a ben 166: un fattore di 33 volte! Chiara dimostrazione dell’infondatezza (parlerei più che altro di distacco dalla realtà) degli autori dell’articolo.

La motivazione è a dir poco banale: tutte le architetture hanno, col tempo, aggiunto nuove istruzioni all’ISA proprio perché c’era (e c’è ancora) bisogno di migliorare le prestazioni in presenza di loro precisi pattern di esecuzione, pur essendo tali nuove aggiunte usate raramente complessivamente.

La soluzione ottica: una sola istruzione

L’approccio scelto per il processore ottico è, invece, diametralmente opposto: viene proposta un’architettura con una sola istruzione, SUBLEQ. La quale prevede quattro parametri (A, B, C, e D per semplicità) e funziona in maniera abbastanza semplice: calcola A – B, mette il risultato in C, e salta alla locazione D se C dovesse risultare minore o uguale a zero.

Ora, è chiaro che con tale istruzione sia formalmente possibile eseguire qualunque tipo di calcolo (è un sistema Turing-completo), com’è anche evidente dall’elenco delle istruzioni direttamente emulabili (riportate nella tabella II della pubblicazione), ma a fronte dell’onerosissimo prezzo da pagare in termini di velocità d’esecuzione.

Questo perché istruzioni più complicate richiederanno parecchie SUBLEQ per poter ottenere risultati equivalenti. L’articolo riporta una sorta di confronto con l’insieme di istruzioni base di RISC-V (chiamato RV32I):

This makes SUBLEQ one of the simplest instruction set architectures (ISA) to implement, while retaining the full ability to run, for example, RISC-V RV32I instructions with a simple translation layer[50].

ma, andando a controllare la pubblicazione citata (nella nota 50) alla pagina 6, la situazione risulta di gran lunga peggiore di quanto sia stata artatamente abbellita. Poco più della metà delle 37 istruzioni RV32I hanno degli equivalenti relativamente semplici (richiedono da 4 a 16 SUBLEQ utilizzate ed eseguite), ma tutte le altre hanno bisogno di molte più istruzioni e, soprattutto, richiedono l’esecuzione di cicli interni per emularne il funzionamento, che fanno letteralmente esplodere il numero di istruzioni effettivamente eseguite e, di conseguenza, comportano un enorme impatto prestazionale (i numeri sono piuttosto eloquenti).

L’articolo parla spesso della possibilità di poter eseguire Windows e più volte fa riferimento a Doom (“ci gira Doom?“, ma tace (e con buona ragione, a questo punto) sulla velocità alla quale potrebbero essere eseguiti…

Oltre alle istruzioni si riducono i bit: solo 16 alla volta

La pensatissima cura dimagrante si estende anche al numero di bit che si possono manipolare alla volta, sebbene in questo caso la pubblicazione non arriva a livelli così radicali, ma ricorda i tempi d’oro dei pionieri informatici della prima metà degli anni ’80.

Infatti dopo una notevole digressione su quanto sia bello avere a che fare con pochi bit (rispolverando pure i gloriosi anni ’70), con tanto di suggestivi consigli di evitare pure i numeri in virgola mobile per affidarsi ai cari, vecchi, interi a 4 o 8 bit (sto semplificando & romanzando, ma il senso dovrebbe essere ben chiaro), citando il recente trend per i calcoli di IA, viene spiattellata la soluzione proposta per i processori ottici:

For an all-optical processor using the current generation of PIC technologies, a 16-bit wide arithmetic unit strikes a good balance between performance and gate count.

Il che dimostra, a mio modesto avviso, che non sia tanto la voglia di portare nell’informatica puramente computazionale il motto “less is better“, quanto si debba fare di necessità virtù, come si suol dire, causa evidenti limiti dell’attuale tecnologia ottica.

Perché non si può certo biasimare l’elettronica che sforna chip con miliardi di transistor (racchiusi in pochi cm2), salvo poi proporre soluzioni facenti uso di qualche centinaio di “gate” (ottici) per risparmiare sulla complessità delle operazioni aritmetiche (che è poi è soltanto una: la sottrazione): la faccenda puzza non poco.

Scelta che viene giustificata dal fatto che 16 bit sarebbero sufficienti nella maggior parte dei casi:

16-bit integers are wide enough, that the majority of the numbers that are currently represented by 32-bits or 64-bits in software experience a minor performance hit, as they rarely exceed a value of 65,535 (examples include most counters in loops, array indices, Boolean values or characters). Should the need arise to represent a 32-bit or 64-bit value, the performance loss is on a manageable order of magnitude for individual arithmetic operations.

che, però, non viene corroborata da alcuna fonte (sebbene la pubblicazione ne sia piena, da altre parti). Non si sa, quindi, da dove abbiano tirato fuori questi numeri, che potrebbero essere in parte condivisibili se fossero ristretti ai valori immediati utilizzati nelle istruzioni (esistono diversi studi in merito, anche per quanto riguarda gli spiazzamenti/offset nelle istruzioni che referenziano la memoria), ma che certamente non posso riguardare i valori manipolati dai processori.

Esempi in tal senso (che contraddicono la tesi) se ne possono fare a bizzeffe, nei campi più disparati: dai database (id per le chiavi?) agli editor di testo (mai capitato di avere a che fare con file aventi più di 65536 righe?), alle scene & asset dei videogiochi, e si potrebbe continuare ancora.

Il suggerimento, in questi casi, sarebbe di concatenare valori a 16 bit fino a coprire le esigenze, ma i costi non sono affatto gestibili da un’architettura che presenta una sola istruzione per fare tutto (quando le architetture attuali mettono a disposizione flag e/o istruzioni esclusivamente allo scopo di gestire meglio questi casi). Come, peraltro, dimostrato dal suddetto studio dell’utilizzo di SUBLEQ per emulare le poche istruzioni dell’ISA base di RISC-V.

A tal fine è bene evidenziare che il motto “ci gira Doom” non ha certamente vita facile con un’architettura del genere, considerato che è stato un netto punto di rottura coi giochi del passato (dove 16 bit sarebbero anche potuti bastare), richiedendo esplicitamente l’uso di 32 bit.

D’altra parte non poteva essere altrimenti, con le coordinate della scena rappresentate da interi in virgola fissa a 16 + 16 bit. Che, però, si scontrano pesantemente con la loro manipolazione, per quanto già detto in precedenza (le operazioni di scorrimento, specialmente quelle a destra, sono un disastro in termini di istruzioni necessarie alla loro emulazione e, soprattutto, a quelle effettivamente eseguite).

A cui va aggiunto il fatto che, tolti i giochi (e le applicazioni) degli home computer di fine anni ’70 e prima metà degli anni ’80, c’è stato bisogno di poter utilizzare ben più di 64kB (Doom ha richiesto ben 4MB come requisito base).

Da questo punto di vista non è affatto chiaro in che modo il nuovo sistema ottico potrebbe indirizzarne più di quanti previsti dall’uso di soli 16 bit. La tecnica del bank switching era in voga verso la metà degli anni ’80, ma sfruttare la memoria in questo modo era complicato e presentava anche un impatto in termini prestazionali.

Sempre relativamente alla memoria, gli autori della pubblicazione ignorano forse che avere puntatori più grandi di 16 bit consente, eventualmente, di utilizzarne alcuni per mappare delle utili informazioni grazie alla tecnica del pointer tagging. Un magistrale esempio in tal senso è rappresentato dall’implementazione di iOS 7 da parte di Apple, grazie alla versione personalizzata dei suoi processori ARM64.

Ma si allargano gli opcode delle istruzioni

La scelta della dimensione standard/naturale dei dati, unita al funzionamento dell’unica istruzione a disposizione, comportano la definizione dell’unico opcode in grado di essere gestito dalla macchina, che per forza di cose dovrà risultare un po’ “cicciottello” (occupando parecchio spazio), ma estremamente facile da decodificare:

As important as improving arithmetic throughput is, the most important aspect about our choice of 16-bit width is, that it is sufficient to enable fixed width instructions. This is vital to reduce the complexity, as it reduces one of the more problematic parts to implement on a processor, the instruction decoder, to a mere look-up table. 

E’ molto facile fare qualche calcolo per capire come sia strutturato quest’opcode. Infatti SUBLEQ ha quattro operandi, ognuno in grado di referenziare una qualunque locazione di memoria dello spazio d’indirizzamento a 16 bit. Dunque occuperà 4 x 16 = 64 bit = 8 byte.

Si tratta di un valore notevolmente elevato se confrontato con quello di altre architetture (che hanno una lunghezza media delle istruzioni che si aggira sui 3-4 byte circa; con poche eccezioni di soli 2 byte), controbilanciato in parte dalla notevole flessibilità data dalla possibilità di referenziare allo stesso tempo tre operandi in memoria.

Purtroppo ciò non è sufficiente a risolvere le problematiche dovute alla semplicità estrema di tale singola istruzione, che comporta la scrittura di parecchie istruzioni per realizzare compiti che altre architetture risolvono con una quantità di gran lunga minore, facendo lievitare grandemente la dimensione delle applicazioni.

Inutile dire che ne risentirà parecchio la densità di codice (parametro molto importante per valutare la bontà di un’architettura), come pure la lunghezza massima dei programmi che potranno girare su siffatto processore (in 64kB dovranno starci sia il codice sia i dati: roba da processori a 8 bit degli anni 70!).

E si riduce la “generalità”: nuovo modello di memoria

La gestione & tipologia di memoria va ad aggiungersi alla lista di limitazioni della computazione ottica. Il modello che viene fortemente spinto, infatti, è quello della memoria a sola lettura, la quale si allinea perfettamente al modello ottico.

Le considerazioni fatte sono certamente valide, perché sappiamo che il codice è generalmente immutabile (lasciamo perdere per il momento le vecchie pratiche riguardo il codice automodificante) come pure lo sono la maggior parte degli asset/dati, mentre una minima parte richiede cambiamenti durante l’esecuzione.

Sorgono, però, due problemi con un modello del genere. Il primo è che, sebbene non cambino in tempo d’esecuzione, il codice e la maggior parte dei dati sono comunque generati e devono essere memorizzati da qualche parte e in qualche modo.

La soluzione a questo problema è il ritorno alla tecnologia WORM (Write Once Read Many), quindi scrivere una sola volta sul supporto di massa (e questo si può fare molto velocemente) e leggere tutte le volte che si vuole (anche qui a piena velocità).

Il secondo problema è che non si possono certo buttare via i supporti WORM ogni volta che codice e/o dati vengono aggiornati (ricordiamo che il codice è, generalmente, immutabile soltanto durante la sua esecuzione. Idem per gli asset).

La soluzione è utilizzare la tecnologia RWORM (Resettable WORM), che consente di effettuare una ripulitura del supporto in modo da essere pronto a riutilizzato per una (sola) scrittura, ma impiegando un paio di ordini di magnitudo (100 volte) più tempo rispetto a una normale scrittura.

Per mitigare questa problematica è proposta l’adozione di un meccanismo di tipo COW (Copy-on-write), la quale prevede di marcare le locazioni (o, in generale, blocchi) di memoria che sono state modificate, per procedere alla loro copia (con le parti modificate applicate successivamente, nei modelli COW comunemente impiegati).

Per la sua implementazione vengono menzionate due possibilità: un aiuto hardware per applicare il COW nel momento in cui avvengono le modifiche, oppure una soluzione interamente software che scarica sui compilatori la responsabilità di individuare le parti di codice che effettuano le modifiche alla memoria e applicare il COW.

Il suggerimento è di adottare la seconda (la prima pare essere troppo complicata/onerosa da implementare), ma in questo modo si dovrebbe cambiare anche il paradigma di programmazione. Posto che abbiamo visto cosa succede quando si delega troppo ai compilatori (vedi Intel e HP con Itanium).

Costringendo a un solo paradigma di programmazione (funzionale)

Manco a dirlo, viene proposta l’adozione del paradigma funzionale, i cui linguaggi che lo implementano sono naturalmente portati verso la gestione della memoria in termini di immutabilità.

Viene citata anche la possibilità di utilizzare linguaggi come C++ e Python in termini (puramente) funzionali, ma sappiamo che non sono certo indicati allo scopo, pur mettendo a disposizione costrutti sintattici e/o librerie allo scopo.

Non è, insomma, naturale e nemmeno tanto comodo scrivere codice usando questi linguaggi, ma legandosi le mani e sfruttando soltanto le parti funzionali. A questo punto è ovvio che la scelta migliore sarebbe affidarsi a uno dei linguaggi che sono naturalmente funzionali.

Rimarrebbero in ogni caso degli scenari in cui è necessario scrivere in memoria (ad esempio per l’I/O, che viene citato anche nella pubblicazione), per cui alla fine anche il processore ottico avrà bisogno di un po’ di memoria convenzionale (volatile) allo scopo (la cui assenza potrebbe, tra l’altro, impattare negativamente sulle prestazioni).

Alla fine un dispositivo di questo tipo avrà bisogno in ogni caso di tre tipi di memoria: ROM per le informazioni assolutamente immutabili, RWORM per immagazzinare la maggior parte delle informazioni (codice e dati/asset) e una parte “volatile” più piccola per le tradizionali locazioni che sono soggette a cambiare valori (troppo spesso).

Complicazione non da poco, di cui dovranno farsi carico i programmatori e i compilatori: certamente non il massimo per un sistema che si propone come “generale”.

Conclusioni

La pubblicazione continua con altre considerazioni sull’utilizzo dell’ottica per implementare operazioni aritmetiche in maniera più efficiente, oppure sui dettagli implementativi, modifiche (pesanti) allo stack software, ecc., sulle quali si potrebbe continuare ancora a discutere, ma preferisco chiudere qui l’analisi perché penso che quanto già visto sia più che sufficiente a comprendere tutti i limiti di questi processori ottici.

Al di là di questo, gli autori si soffermano molte volte sulla fattibilità quando nella pubblicazione si parla di certe scelte e di limiti che farebbero storcere il naso a chiunque. Il punto è che se un dispositivo come questo è Turing-completo, non v’è dubbio che sarebbe potenzialmente in grado di effettuare qualunque tipo di calcolo.

Ciò, però, non implica certo che possa essere considerato di utilizzo “generale”, per come lo intendiamo normalmente, in quanto dev’essere in grado di assolvere ai compiti che vengono dati in pasto agli attuali dispositivi elettronici, ma completandoli meglio (con minor consumo energetico e più velocemente).

La teoria, insomma, rimane fine a sé stessa anche nella misura in cui vengono presentati dei prototipi e snocciolate le loro caratteristiche.

Meglio la pratica: ci gira Doom (come più volte citato)? Allora voglio vederlo girare meglio (più velocemente) degli attuali sistemi e consumando meno energia. Altrimenti si rimane nell’ambito delle chiacchiere (come per i fantomatici computer quantistici che dovrebbero rimpiazzare quelli odierni).

Press ESC to close