Come abbiamo già avuto modo di vedere nei precedenti articoli dedicati a questa splendida architettura, ARM ha avuto particolarmente a cuore le esigenze dei suoi partner commerciali (i licenziatari delle licenze delle sue CPU), introducendo numerose estensioni orientate a particolari funzionalità.
Quest’attenzione non è stata certo frutto di un amore incondizionato e gratuito, quanto una necessità perché, per quanto interessanti fossero, i suoi microprocessori non hanno avuto il successo sperato nel mercato in cui Acorn (l’azienda ideatrice del progetto) operava principalmente: quello desktop.
Dovendo ripiegare nel settore dei dispositivi embedded, ne ha condiviso le problematiche e le aspettative, e fra queste quelle di una maggior densità del codice (per occupare, quindi, meno spazio) che l’ha portata a sviluppare Thumb, nuova ISA a 16 bit. Visto il successo che ha avuto, era lecito aspettarsi una sua evoluzione, ma Thumb-2, com’è stata poi chiamata, ha portato ben più che l’aggiunta di qualche istruzione: è arrivata addirittura un’ISA a 32 bit!
Ciò 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 datasheet, notiamo che la tabella degli opcode è quasi interamente utilizzata (gli spazi liberi rimasti sono pochi). Eppure ARM proclama la possibilità di eseguire, in questa modalità, allo stesso tempo le istruzioni Thumb a 16 bit e quelle nuove a 32 bit.
In realtà non tutte le istruzioni Thumb erano a 16 bit: alcune erano, a conti fatti, a 32 bit. Esistono due istruzioni di salto, BL e BLX, che permettono di specificare un offset di 4MB (con segno, quindi da -2MB a +2MB). Poiché 4MB richiedono 22 bit per essere rappresentati, ovviamente 16 bit non erano sufficienti.
ARM ha optato per “spezzare in due” quest’offset, delegando a una speciale istruzione di “prefisso” il contenimento degli 11 bit alti, e alle due istruzioni vere e proprie di portarsi dietro gli 11 bit bassi. In questo modo riesce a ricostruire il valore a 22 bit che gli serve per generare l’offset.
L’esecuzione rimane comunque di due istruzioni a 16 bit, coi rispettivi tempi. Per fare un esempio, se dev’essere eseguita un’istruzione BL, il compilatore avrà generato due istruzioni: quella di prefisso e BL stessa. Quando il processore incontra la prima istruzione memorizza (in qualche buffer interno) soltanto gli 11 bit che contiene; non fa altro. Passa quindi a quella successiva (BL), che preleva i suoi 11 bit di offset, poi quelli memorizzati in precedenza, li combina, ed esegue il salto vero e proprio.
Si tratta di una soluzione particolarmente ingegnosa per non aumentare eccessivamente la complessità del decoder. Complessità necessaria se avesse dovuto distinguere fra le istruzioni a 16 bit e queste speciali 32 bit, eseguendo queste ultime “in un colpo solo”. In tal modo ARM ha dato, come ci ha ormai abituato, il classico colpo al cerchio e alla botte, introducendo la possibilità di eseguire salti “lunghi”, con una leggera perdita di prestazioni (deve pur sempre eseguire due istruzioni), ma con un risparmio nella circuiteria (consumi inclusi).
Rimane però il problema della tabella degli opcode particolarmente affollata e con poco spazio per introdurre ulteriori istruzioni a 16 bit. Figuriamoci, quindi, come sarebbe possibile introdurne di altre, addirittura a 32 bit. La soluzione trovata sta nell’utilizzare proprio gli opcode speciali di salto per mappare tutte le nuove istruzioni a 32 bit, sfruttando un “vuoto” lasciato proprio dal meccanismo precedentemente spiegato.
Il concetto è tanto semplice quanto geniale. Quando viene eseguita l’istruzione di prefisso, il microprocessore si aspetta necessariamente che la successiva istruzione sia una BL o una BLX, pena il passaggio a una situazione di impredicibilità (il comportamento della CPU non è specificato, ed è lasciato alle singole implementazioni). Lo stesso vale se si tenta di eseguire una BL o BLX prima di quella di prefisso.
Sfruttando queste aree grigie, ARM le ha trasformate in nuovi opcode a 32 bit (abbastanza) liberamente utilizzabili per mappare le istruzioni della nuova ISA (mi permetto di chiamarla così, anche se tecnicamente non lo è, perché a tutti gli effetti si comporta come tale; vedremo poi perché).
Quindi se il processore si trova davanti un’istruzione di prefisso, e quella successiva non è BL o BLX, sa che questa è un’unica, nuova, istruzione a 32 bit (derivata dalla combinazione delle due a 16 bit). Idem se si trova immediatamente davanti una BL o BLX: sa che si tratta di istruzioni a 32 bit, e provvede a combinarle direttamente con il successivo valore a 16 bit (che non è, quindi, un’istruzione: è un valore vero e proprio).
In questo modo la tabella degli opcode è stata notevolmente “allargata”, e ciò ha permesso ad ARM di raggiungere l’obiettivo che s’era prefissata: integrare in Thumb tutte le istruzioni dell’architettura originale. Quindi adesso è possibile sfruttare direttamente i coprocessori, le estensioni SIMD, utilizzare tutti i registri (mentre prima l’accesso diretto era riservato soltanto ai primi otto), e in generale tutte le istruzioni più complesse che l’ISA ARM mette a disposizione (comprese quelle per gestire le eccezioni e gli interrupt).
L’unica cosa che rimane fuori è la possibilità di esecuzione condizionata del codice (in base ai flag del registro di stato), che però era anche il fiore all’occhiello di quest’architettura. Purtroppo non era rimasto proprio spazio: mancano proprio i 4 bit che servono per specificare la condizione. Per il resto c’è veramente tutto, e… anche di più!
Infatti per l’occasione ARM ha pensato bene di arricchire l’ISA di Thumb, aggiungendo alcune istruzioni (la maggior parte anche per quella ARM) per:
- caricare costanti immediate a 16 bit (quindi combinando due opportune istruzioni è possibile caricare costanti a 32 bit, vera piaga per i RISC)
- manipolare campi di bit (cancellazione, inserimento ed estrazione con o senza segno)
- invertire i bit di una word (utile per calcolare gli indici nell’FFT)
- eseguire salti utilizzando una tabella di offset (niente indirizzi assoluti, ma quantità a 8 o 16 bit estese con segno, che vengono sommate al PC per eseguire opportuni e comodissimi salti relativi di tipo tabellare)
- controllare se un registro è uguale a zero (oppure no), ed eventualmente saltare
Dicevo prima che era rimasta fuori la possibilità di eseguire istruzioni condizionate, ma ciò non è del tutto vero. Essendo una caratteristica molto apprezzata degli ARM, è stata reintrodotta, sebbene in una forma un po’ diversa. Oltre alle altre, è stata aggiunta un’istruzione chiamata “If-Then“ (IT; ma, per come funziona, la chiamerei ITF: “If-Then-Else“) il cui scopo è di specificare una condizione da applicare fino a 4 istruzioni da eseguire in base a essa.
Un esempio (preso dalla documentazione ARM) sarà utile a chiarirne il funzionamento:
Il codice ARM prevede l’esecuzione di LDREQ e ADDEQ solo se la condizione (EQ) risulta vera, e di LDRNE e ADDNE nel caso esattamente opposto.
Il codice Thumb lo esplicita facendo ricorso al classico salto condizionato (ed eventualmente uno incondizionato) sulla base della condizione che serve quindi a “dividere” (diramare, per la precisione) il flusso dell’esecuzione.
Quello Thumb-2 è il più oscuro, ma si spiega facilmente prendendo l’istruzione utilizzata, ITETE, e considerando che T = Then ed E = Else. Quindi le seguenti 4 (TETE) istruzioni vengono eseguite sulla base della condizione EQ (di equivalenza o uguaglianza a zero) per la prima (che è sempre T) e la terza, mentre di quella opposta (NE, non equivalenza o diverso da zero) per la seconda e la quarta.
A conti fatti il codice ARM viene eseguito in 4 cicli di clock (e occupa 16 byte), quello Thumb da 4 a 20 cicli (prendendo 12 byte; ma sfortunatamente può causare lo svuotamento della pipeline), mentre Thumb-2 impiega 4 o 5 cicli (e occupa 10 byte). Un ottimo risultato e… la pipeline ringrazia (nessun pericolo di svuotamento)!
Si tratta complessivamente di aggiunte particolarmente utili (un po’ meno quella del bit reversal, che è appunto usato nell’FFT: quindi poco generale come utilizzo), e volte a cercare di compattare ancora di più il codice, migliorando ovviamente anche le prestazioni (che prima rappresentavano il tallone d’Achille di Thumb). La casa madre dichiara che (mediamente) il codice compilato in Thumb-2 occupa il 74% dello spazio rispetto all’equivalente ARM e prestazioni simili (vicine al 98%).
Il costo da pagare lo si ritrova in una maggior complessità dell’ISA e in particolare a carico del decodificatore delle istruzioni, che adesso si trova ad avere a che fare con istruzioni di lunghezza variabile (sebbene limitate a 2 o 4 byte; quindi mezza word o una intera), ma col problema dell’allineamento (un opcode a 32 bit si può benissimo trovare in memoria in mezzo a due word, per cui devono essere lette entrambe prima di procedere alla completa decodifica).
Si tratta, come scrivevo anche nel titolo, di un ritorno al passato e più precisamente ai CISC, a cui l’ARM si era in ogni caso ispirata (al 6502 della gloriosa MOS), come sottolineavo nell’articolo sul primogenito della specie.
Non starò qui discutere di RISC e CISC (argomento previsto per un futuro articolo), la cui linea di demarcazione col tempo è andata via via assottigliandosi, ma non credo di sbagliare se affermo che un elemento di netta distinzione fra i microprocessori delle due famiglie è sempre stata la presenza di ISA con opcode a lunghezza fissa nel primo caso e variabile nel secondo. Thumb-2 è, a tutti gli effetti, un appartenente alla seconda tipologia e, dunque, mi sento di classificarla come ISA CISC.
Un doveroso appunto lo devo fare perché all’inizio ho parlato di nuova ISA, anche se formalmente ci troviamo davanti a un’estensione della precedente Thumb. Il motivo è presto detto: i cambiamenti sono stati tanti e tali che mi hanno portato a questa forte affermazione. Considerate che, fatta eccezione per l’esecuzione condizionata, tutte le istruzioni dell’ISA ARM sono state opportunamente mappate nei nuovi opcode Thumb-2!
Qui farei un appunto ad ARM. Dando un’occhiata alla tabella degli opcode ARM e a quella Thumb (a 16 bit), a mio avviso tutte le istruzioni ARM eseguite condizionatamente si sarebbero potute mappare tramite i due opcode a 32 bit derivanti dalla coppia di istruzioni BL e BLX, che assieme mettono a disposizione 28 bit e cioè 32 meno i 4 riservati ai flag condizionali:
Mentre le rimanenti istruzioni non condizionate (che fanno uso della “condizione” 0b1111) le si sarebbe potute mappare con l’opcode a 32 bit introdotto con l’istruzione di prefisso BL/BLX, che mette a disposizione altri 27 bit (tranne le configurazioni regolarmente utilizzate da Thumb per BL e BLX, che in ogni caso portano via poco spazio nell’opcode).
Ciò (sempre se le mie ipotesi fossero confermate) avrebbe permesso di semplificare il decoder, perché si sarebbe potuta riciclare buona parte di quello utilizzato per l’ISA ARM, mentre adesso è presente della logica apposita, visto che le istruzioni ARM importate in Thumb-2 hanno una mappatura per lo più diversa.
Si tratta di speculazioni di chi magari guarda troppo alla perfezione, ma ciò che conta è che con questa mossa ARM s’è sicuramente portata molto avanti, come vedremo meglio quando, a breve, parleremo di una nuova famiglia basata su quest’architettura che fa capo al Cortex-A8…