L’architettura ARM ha subito consistenti cambiamenti nel corso della sua storia, come abbiamo cercato di evidenziare nel precedente articolo .
Uno dei più grossi, ma poco noti e/o apprezzati, è rappresentato dall’introduzione di nuove modalità di esecuzione del microprocessore, che corrispondono effettivamente a delle nuove ISA implementate e, quindi, a decoder appositamente dedicati per le istruzioni codificate in questi nuovi formati.
Non era certamente un passo obbligato per ARM Ltd, essendo la sua un’architettura ben delineata, ma, essendo un’azienda particolarmente attenta alle esigenze del mercato, s’è trovata davanti non a un capriccio, quanto alla necessità tutt’altro che remota di migliorare la densità del codice, a cui ha dato seguito integrando nella quarta versione della sua famiglia quella che ha poi chiamato modalità Thumb.
Come abbiamo già appurato, da diverso tempo quest’azienda opera principalmente nel settore dei dispositivi embedded, arrivando ad abbracciare anche smartphone e PDA (e, di recente, anche i netbook), per i quali il software viene distribuito sotto forma di firmware (eventualmente aggiornabile).
Il firmware è, in genere, una memoria non volatile (EEPROM o Flash) che può variare da pochi MB fino a un centinaio (almeno per ora), e che racchiude tutto ciò che è necessario per l’avvio (reset o reboot) e il caricamento del sistema operativo, visto che ormai da tempo questi gioielli tecnologici sono diventati estremamente complessi e non è più sufficiente un sistema “minimale” (giusto per far partire l’applicazione che si occupava di tutto).
Per essere precisi, un po’ di anni fa non serviva un s.o. con questi aggeggi. Erano talmente semplici che nel firmware era presente soltanto una routine che serviva al setup del sistema all’avvio, che passava poi il controllo a un codice “factotum”, il quale si occupava di gestire qualunque cosa: agenda degli appuntamenti, blocco note, mini foglio di calcolo, sveglia, ecc.
Oggi PDA o smartphone offrono parecchie funzionalità, hanno delle interfacce grafiche sciccose e richiedono pertanto software estremamente complessi per gestire il tutto (anche se non digerirò mai i tempi biblici di accensione dei Nokia). Ovviamente ci sono sistemi embedded che continuano più o meno come prima, perché hanno esigenze ridotte.
In ogni caso la presenza di un firmware su una memoria non volatile comporta un certo costo per i produttori, per cui trovare il modo di ridurre lo spazio occupato è sempre una cosa “buona e giusta”. Coi RISC, però, la storia è sempre la stessa da quando sono nati: avendo degli opcode a dimensione fissa, la dimensione del codice rispetto ai CISC (che usano opcode a dimensione variabile) è mediamente maggiore.
ARM, tanto per cambiare, non fa eccezione. Pur essendo dotata di un’architettura molto flessibile, con esecuzione condizionale del codice, ben 16 registri general purpose, istruzioni con 3 registri, e la possibilità di utilizzare il barrel shifter interno in buona parte delle operazioni di calcolo (come ho spiegato nel primo articolo a esso dedicato), utilizza opcode di 32 bit. Quindi anche l’operazione più semplice (ad esempio una NOP, che non fa nulla) presenta una costo fisso di ben 4 byte.
Per confronto, un microprocessore CISC che utilizza opcode di dimensione variabile minima di 8 bit (come un x86) per le operazioni più semplici richiede soltanto un byte. Un CISC della famiglia 68000, che ha opcode di dimensione minima di 16 bit, ne impiegherà 2, ma è comunque un netto risparmio rispetto ai 4 minimi (e massimi) di un RISC come l’ARM (e tanti altri).
Confronti di questo tipo, però, non vanno fatti sulla sola base della dimensione minima degli opcode (non è questo l’obiettivo dell’articolo), ma servono soltanto per introdurre il concetto. Quella della maggior densità di codice è stato, ed è ancora, uno dei buoni motivi per cui chi lavora in settori embedded preferisce un CISC a un RISC, e a cui aggiungiamo anche la generale maggior facilità di scrittura di codice a basso livello (in assembly), sempre coi primi.
Tutto ciò ha portato ARM, come dicevo, a introdurre la modalità Thumb, con la quale il processore si trova a dover leggere dalla memoria opcode di dimensione fissa a 16 anziché a 32 bit. Conti alla mano, la dimensione minima è scesa a 2 soli byte, risparmiando fino al 35% di spazio per il codice rispetto all’equivalente a 32 bit.
Chiaramente la “cura dimagrante” non ha portato via dei bit “inutili”, per cui l’utilizzo di opcode di dimensione ridotta ha comportato notevoli modifiche e restrizioni all’utilizzo delle caratteristiche presenti nel core, primo fra tutti il numero di registri direttamente manipolabili che è passato da 16 a 8 (i primi, da R0 a R7).
In realtà gli altri non sono stati “fatti fuori”, ma è possibile accedervi con apposite istruzioni (si può copiare il registro R8 in R3, tanto per fare un esempio). Inoltre altri registri sono stati “rimappati” opportunamente e vengono utilizzati più o meno implicitamente, a seconda del contesto.
Il PC (Program Counter, R15) viene comunque sfruttato per recuperare gli opcode; SP (Stack Pointer, R13) si usa per simulare lo stack, e infine LR (Link Register, R14) conserva l’indirizzo di ritorno dalla chiamata a subroutine. A conti fatti, soltanto 5 registri (da R8 a R12) rimangono fuori e richiedono apposite istruzioni per accedervi. 11 registri su 16 rappresenta comunque un buon risultato.
Sul fronte delle istruzioni, manco a dirlo, si trovano i cambiamenti più profondi:
- le istruzioni che possono utilizzare tre registri sono soltanto quelle di somma e sottrazione
- sempre somma e sottrazione sono le uniche che possono utilizzare due registri e un valore immediato (limitato da 1 a 8)
- sono state introdotte apposite istruzioni di shift (prima del tutto assenti: si utilizzava il barrel shifter direttamente nelle istruzioni, se serviva)
- diverse istruzioni consentono di utilizzare un valore immediato a 8 bit (somma, sottrazione, confronto, assegnamento, somma a PC o SP, e infine invocazione a interrupt software)
- sono presenti numerose istruzioni di load/store, che in pratica codificano soltanto alcune (le più utili o necessarie) delle modalità d’indirizzamento utilizzabili con gli opcode a 32 bit
- fanno il loro ingresso i famigerati salti condizionati (con offset limitato a 8 bit)
- i salti incondizionati sono limitati a offset a 10 bit
- i salti a subroutine richiedono l’uso di due istruzioni che, “concatenate” (eseguite una di seguito all’altra), forniscono un offset a 20 bit
A questo punto per chi ha avuto modo di lavorarci o studiarla, la domanda che sorge spontanea è: che fine ha fatto l’elegante architettura ARM? Non c’è, è sparita: questa non ne rappresenta nemmeno l’ombra. E infatti, checché ne dica ARM parlando di Thumb come di una “estensione” della tradizionale ARM, questa rappresenta, a tutti gli effetti, un’altra ISA, e pure molto diversa.
Agli occhi di qualcuno potrà sembrare ingiustificato uno stravolgimento di questa portata, col solo scopo di migliorare la densità del codice, ma l’architettura ARM è comunque utilizzabile (almeno per ora), perché è possibile passare da ARM a Thumb e viceversa in maniera molto veloce, grazie a delle apposite istruzioni.
Inoltre il minor spazio occupato dal codice presenta benefici effetti anche sulla banda di memoria, che risulta ridotta in misura almeno equivalente allo spazio occupato, con risparmi ancora maggiori in presenza di cicli (per i quali viene speso la maggior parte del tempo).
Si riducono anche gli accessi alla memoria, poiché spesso vengono caricate da essa word a 32 bit quando si tratta di trasferire all’interno del core gli opcode da eseguire. Infatti in una word si possono memorizzare due opcode a 16 bit, per cui il secondo non necessita di alcun fetch: si trova già a disposizione.
I puristi continueranno a storcere il naso, in quanto l’architettura risulta del tutto snaturata, ma bisogna essere pragmatici: con Thumb, ARM ha un’ottima carta da giocare nei confronti della concorrenza che da anni offre codice più compatto, consentendo ai produttori di dispositivi basati su questa CPU di risparmiare tanti soldi.
Turiamoci il naso (per chi proprio non ce la fa) e andiamo avanti…
Beh, il concetto su cui si basa la modalità Thumb non deve essere poi così disprezzabile se anche STMicroelectronics con la famiglia ST100 ha fatto la stessa cosa ;)
Ciao
Filippo
A quanto pare è un problema che si sono poste diverse case produttrici di RISC. :D
Ma io sono convinto che in linea di principio l’idea sia assolutamente degna di nota. Poi nelle specifiche implementazioni possiamo discutere, si sarà intuito che ho avuto esperienze con la famiglia ST100 di STM e francamente l’equivalente della modalità Thumb era parecchio restrittivo, tanto che alla fine spesso e volentieri si era costretti a compensare la minore flessibilità dell’ISA “castrata” usando più istruzioni, e quindi di fatto la lunghezza complessiva del codice non diminuiva di molto, perlomeno non sempre. Bisognava lavorarci sopra parecchio per ottenere buoni risultati…
Ciao
Filippo
Stamattina ho cercato il datasheet dell’ST100, per dare un’occhiata all’architettura, ma non si trova nemmeno nel sito dell’ST! :(
Comunque concordo con te, e i miei commenti penso siano eloquenti: non è semplicemente una castrazione, ma uno snaturamento dell’architettura “originale”. Nel Thumb non c’è nulla che richiami alla mente l’ISA ARM e le sue peculiarità: è un altro processore, a tutti gli effetti.
I vantaggi ci sono (e per questo reputo anch’io apprezzabile la soluzione), perché, anche dovendo usare più istruzioni in alcuni casi per fare le stesse cose, mediamente si ottiene un risparmio rispetto allo stesso codice interamente scritto con l’ISA originale.
Non so con l’ST100 come sia la situazione (mi sarebbe piaciuto approfondire), ma nel tuo caso lavoravi in assembly? Perché ormai anche per i sistemi embedded va di moda il C (o addirittura il C++), quindi ci si affida alla bontà del compilatore nel tirare fuori codice che occupi meno spazio.
Purtroppo mi risulta che l’architettura ST100 non sia più ufficialmente supportata da STM. E’ un peccato, perché se ben utilizzato, era un bel DSP.
Per questo motivo non troverai datasheet nel sito STM. Ho trovato questo, su un sito esterno:
http://www.alldatasheet.com/datasheet-pdf/pdf/208335/STMICROELECTRONICS/ST100.html
Comunque sì, lavoravo in assembly. All’epoca (primissimi anni 2000) la toolchain C/C++ dell’ST100 era stata appena messa “in strada” e quindi essendo giovanissima non era (ancora) molto efficiente. Aggiungi che l’architettura ST100 non è ciò che si dice propriamente “compiler friendly” :-), principalmente a causa di una complessa architettura load/store che se propriamente utilizzata forniva un throughput impressionante nei cicli DSP, ma per contro richiedeva una accuratissima schedulazione delle istruzioni a causa della elevata variabilità delle latenze (da 1 a 7 cicli di clock, le più lente erano le istruzioni di copia tra registri dati e registri indirizzi).
Ripeto, dal mio punto di vista è un peccato che non viva più quest’architettura perché aveva fatto ancora più di quello che tu hai qui illustrato a proposito di ARM, l’ST100 offriva non due ma ben tre ISA switchabili “a caldo” con opportune istruzioni macchina: GP16 (GP sta per general purpose, la GP16 è l’equivalente della Thumb di ARM), GP32 e SLIW, a seconda che volessi privilegiare la densità del codice (GP16), il throughput (SLIW, modalità nella quale era possibile schedulare 4 istruzioni per ciclo) o fare una via di mezzo (GP32).
Ciao
Filippo
Sì, queste cose le avevo già lette stamattina proprio dal link che hai riportato, ma purtroppo si tratta di una paginetta in cui sono racchiuse soltanto delle informazioni generali (comunque molto interessanti).
Avrei preferito il datasheet completo con la spiegazione dell’intera architettura della CPU, per capire meglio come funzionava, e i punti di forza e di debolezza. Purtroppo non si trova proprio niente.
Comunque ho qualche amico all’STM: magari riesce a recuperarlo lui.
Sempre all’STM ricordo che avevano un’altra architettura che, penso, abbia soppiantato l’ST100: si chiama LX, ed è un VLIW in grado di eseguire 4 istruzioni in bundle per ciclo di clock. Ricordo di averci compilato il mio decoder JPEG 2000, ma non ho altre informazioni su quest’architettura.
Tornando all’ST100, mi sembra strano che le istruzioni avessero latenze così variegate. In genere i DSP tendono a eseguire tutte le istruzioni in 1 ciclo di clock, in modo da rendere più facile il calcolo del tempo d’esecuzione dell’algoritmo.
Sembra che l’STM abbia voluto realizzare una CPU un po’ più general purpose, per cercare di utilizzare il progetto in ambiti applicativi di più ampia portata.
L’LX è in sostanza quello che internamente era chiamato ST200, un’architettura realizzata in collaborazione con HP: la stessa che, con Intel, aveva poi portato alla realizzazione dell’Itanium, che guarda caso è accomunato all’LX di STM dallo stesso paradigma (il Very Long Instruction Word).
Mi ricordo che l’ST100 e l’LX risalgono, come concezione, più o meno allo stesso periodo o giù di lì, e quindi si era bene o male innescata una “competizione” tra sostenitori dell’una o dell’altra “fazione” :-) All’epoca stavo lavorando ad un CODEC MPEG-1 Layer II (lo standard di compressione audio usato tra l’altro, se non sbaglio, per la codifica dell’audio della TV digitale). Era stato fatto un porting di questo CODEC anche sull’LX, e ci si imbeccava a colpi di amichevoli insulti per via del conteggio dei cicli macchina necessari per una singola chiamata del filtro a sottobande (l’equivalente della trasformata discreta coseno per il video, in pratica il cuore del codificatore audio). L’LX ha un parallelismo grossomodo uguale a quello dell’ST100 in modalità SLIW (4 istruzioni per ciclo), ma se non sbaglio di queste quattro istruzioni solo una poteva essere di load/store nell’LX, mentre nell’ST100 potevano essere due (purché i dati su cui fare il load fossero opportunamente organizzati nella memoria: due load simultanee non erano sempre possibili).
Per quanto riguarda la latenza molto varia, in effetti è un po’ un’anomalia per un RISC. Personalmente non conosco nel dettaglio le ragioni tecniche per le quali alcune delle istruzioni fossero sensibilmente più lente delle altre: tieni comunque conto che le istruzioni più pesanti in termini di latenza erano quelle svolte dall’unità di controllo (l’architettura ST100 prevede un’unità di controllo, un’unità di load/store e due o più ALU), ovvero i branch e, come detto, le copie tra registri dati e indirizzi (che se non ricordo male sono deputate all’unità di controllo). Anche per questo l’ST100 offriva lo strumento delle “guarded instructions”, in altre parole si poteva condizionare la scrittura del risultato di un’istruzione allo stato di un apposito registro flag. L’istruzione veniva eseguita comunque, ma il suo risultato veniva “consegnato all’oblio” se il flag del registro guard non era TRUE. Era pur sempre una penalità meno pesante della latenza che conseguiva ad un JUMP :-)
E poi non dimentichiamo (non so se altre architetture offrissero funzionalità analoghe) il supporto zero-latency per i loop (fino a tre annidati) e l’indirizzamento circolare (caratteristiche che però richiedevano l’allineamento di codice ed istruzioni a ben precisi multiply di byte e quindi, anche qui, il codice andava pianificato accuratamente).
Insomma, volendo ce n’è di carne al fuoco… un’architettura ingiustamente sottovalutata secondo me.
Ciao
Filippo
Dopo ciò che hai scritto, sono del tuo stesso parere, e alimenta ancora di più il mio desiderio di approfondire la conoscenza di queste CPU. :D
Tra l’altro molte delle cose che hai elencato (le istruzioni “guarded”, ad esempio) le ritroviamo nell’Itanium / EPIC sviluppato da HP e Intel, che hai citato. E non a caso, a questo punto.
Comunque “a naso” l’ST100 mi sembra messo dell’LX/ST200. Urgono datasheet di entrambi per appurarne le peculiarità. :P
reputo che sia un passo indietro rispetto al passato, in quanto il futuro è la semplificazione dei microcode, pertanto qui abbiamo forse una funzione ibrida che abbandona la vecchia architettura in ragione di una nuova piu’ adeguata alle esigenze e forse piu’ performante. purtroppo si tende a mantenere anche la vecchia per la triste pratica della compatibilità come è successo nel passato con i processori intel, personalmente avrei inserito un sistema di compressione/decompressione onfly del codice a monte della cache, avremmo recuperato circa il 50% dello spazio sui firmware e di fatto raddoppiato la banda di memoria, quindi l’accesso alla memoria sarebbe stato in dual mode utilizzabili in contemporanea:
1. compress
2. normal
mentre il micro code sarebbe rimasto uguale…
avrebbe avuto sicuramente prestazioni prestazioni migliori e una riduzione di spazio.
pazienza un’occasione persa.
Non ho idea di come avrebbe funzionato questa compressione, ma Intel ha provato già in passato una sorta di compressione degli opcode, fallendo miseramente: http://www.appuntidigitali.it/4151/iapx-432-il-primo-processore-a-32-bit-di-intel-a-oggetti/
Il problema in questi casi è che la circuiteria risulta molto più complessa e rappresenta un collo di bottiglia (magari adesso no: ma all’epoca Intel ha dovuto separare la logica di fetch & decode, mettendola su un chip dedicato) per il sistema.
Tu cosa proporresti? Come funzionerebbe il tuo modello?
@ homero
Hai dei riferimenti all’efficienza della tecnica di compressione del codice (paper, ecc.)?
Hai dei riferimenti per quel 50%? Mi sembra veramente tanto.
La cosa che mi preoccupa, cosi’ su due piedi, e’ il tempo di decompressione, che si aggiunge secco alla miss penalty di accesso alla memoria.
A naso, avresti piu’ banda verso la memoria, ma anche piu’ latenza.
PS: sempre su due piedi, mi preoccuperei anche dell’energia necessaria alla decompressione, considerando che si parla di sistemi embedded, molto attenti al consumo energetico.
Bisognerebbe confrontare l’energia spesa per la decompressione con quella risparmiata nel minor numero di accessi alla memoria.
La latenza penso che si potrebbe mascherare con l’allugamento delle pipeline. Rimarrebbe il maggior consumo ovviamente, perché la logica di “decompressione” costa e non può essere disattivata.
C’è però un grosso dubbio, e riguarda l’algoritmo di de/compressione: se è dinamico, cioè produce opcode di lunghezza variabile, si pone il problema dei salti, che diventano molto più complicati da gestire.
@ 13
Si’, ci sono un po’ di problemini, ma la cosa e’ interessante e credo sia (o sia stata) investigata abbastanza (anche in processori “normali”, per ridurre lo spazio usato nella cache).
Purtroppo non ne so nulla, per questo qualsiasi riferimento (paper ecc.) e’ gradito, cosi’ studio un po’ la cosa :)
Perche’ dici che allungare la pipeline aiuterebbe? E’ out-of-order? Per avere altre istruzioni in-flight con cui mascherare la latenza?
Forse piu’ che piu’ stadi aiuterebbe avere piu’ thread in esecuzione, cosi’ all’accesso alla RAM cambi thread. Questo aiuterebbe cmq, indipendentemente dalla decompressione.
E di DSP multithreaded ormai ce ne sono, ad esempio lo Hexagon (l’ultimo DSP di Qualcomm, fatto in casa da loro).
Sì, ci sono appunto queste possibilità per cercare di mascherare la latenza. Questo ove possibile, perché in una CPU multithread il singolo thread verrebbe comunque penalizzato.
Quanto a paper et similia, sono anch’io come te: sempre affamato di materiale da spolpare per acquisire know-how. :P
Comunque la soluzione “migliore” per salvare spazio e banda io la conosco già: si chiama CISC, ma non si concilia molto coi RISC. :D
Se può interessare a Mauro, finché scava nelle sue conoscenze alla ricerca dell’agognato datasheet :D
Parlo sempre dell’ST100 perché come si sarà capito è l’unica architettura che ho potuto conoscere un po’. Questo DSP integrava un meccanismo molto interessante per mascherare la latenza degli accessi alla memoria, sia in load che in store, attraverso un uso furbo delle cosiddette “instruction queue”. In pratica, era possibile mandare in esecuzione nello stesso ciclo una istruzione di load e una operazione ALU che usava come operando il risultato di quella load. Ad un certo momento della decodifica della codeword, l’istruzione ALU veniva messa “in coda” finché la load terminava il suo lavoro, dopodiché l’esecuzione dell’istruzione ALU proseguiva. Il tutto era trasparente allo sviluppatore. Ovviamente nelle prime iterazioni di un loop questo meccanismo introduceva una latenza, ma poi le instruction queue a regime la mascheravano. Risultato, il tutto appariva come se il risultato della load fosse istantaneamente disponibile nel register file (l’ST100, da bravo RISC, non poteva operare se non su registri: niente operazioni miste, che io sappia).
Ciao
Filippo
@ 16
Vergognoso lapsus, chiedo scusa. Volevo dire “Se può interessare a Cesare” :) Sorry! Chissà perché ho scritto “mauro”…
Ciao
Filippo
Non ti preoccupare, ci sono abituato. :D
Grazie della spiegazione. Mi sembra che le CPU moderne out-of-order utilizzino da un po’ di tempo lo stesso meccanismo.
[…] precedente articolo abbiamo introdotto la modalità Thumb, soluzione adottata da ARM con la versione 4 della sua […]
[…] sembrerà quanto meno strano, in quanto sappiamo ormai che Thumb è un’ISA a 16 bit , per cui esegue istruzioni rigorosamente di questa dimensione. Inoltre andando a spulciare sul […]