Qualche giorno fa Agner Fog, un esperto di architetture x86, ha rilasciato sul suo blog un articolo relativo alle problematiche di quest’ISA relativamente alla sua implementazione, estensione e utilizzo da parte dei programmatori.
I punti salienti sono stati riassunti da AnandTech e consistono sostanzialmente nel fatto che ci sono troppe istruzioni (oltre un migliaio secondo l’autore), ne sono presenti ancora di obsolete, la decodifica è complicata, c’è una guerra che si combatte a colpi di estensioni fra i produttori di CPU x86, e infine il supporto di tutte queste aggiunte / variazioni è troppo costoso.
Debbo dire che mi trovo abbastanza d’accordo con quanto espresso (d’altra parte sono cose ben note a chi studia o lavora con quest’ISA), ma ritengo sia necessario effettuare dei distinguo e delle precisazioni, per meglio comprendere la portata di alcune affermazioni.
Che x86 sia un pastrocchio nato male (8086) credo sia un pensiero comune, e pochi sarebbero disposti ad affermare il contrario. Agli enormi limiti strutturali Intel ha posto rimedio nel tempo, estendendo lo spazio d’indirizzamento (80286), portando gli agognati 32 bit (80386), e il concetto di SIMD (le famose MMX), trovando principalmente in AMD un valido avversario che ha introdotto i 64 bit (x86-64) e alcune interessanti estensioni SIMD (3DNow!), con Via infine a fare da gregario ma con qualche contribuito (AES, generazione di numeri casuali, SHA 1 e SHA-256).
Questo processo di estensione ha comportato una rivisitazione della tabella degli opcode, che essendo a 8 bit è arrivata velocemente a saturazione (già con l’8086 gli slot era quasi del tutto assegnati), per cui si è dovuto escogitare il meccanismo dei prefissi per aggiungere nuove tabelle di opcode (sempre a 8 bit) sfruttando i pochi spazi vuoti rimasti. In parte l’argomento è stato trattato in un mio precedente articolo (in cui mettevo a confronto le ISA ARM e x86 in prospettiva dei consumi).
Da qui le istanze di Agner, che rimarca come tutto ciò penalizzi non soltanto le implementazioni, che necessitano di sezioni di decodifica decisamente complesse che comportano un aumento della dimensione del chip e dei consumi, oltre a richiedere specifico supporto da parte degli sviluppatori per poterne sfruttare le capacità (se c’è una nuova funzionalità e non la si sfrutta, o la si sfrutta male, sostanzialmente occupa soltanto spazio e assorbe energia).
In realtà l’argomento decoder delle istruzioni x86 non è così semplice da trattare (come ho sottolineato nell’articolo che ha messo a confronto le unità di decodifica di ARM e x86), e ciò perché le implementazioni più recenti (da un po’ di anni a questa parte) rappresentano già un ottimo nonché flessibile strumento che difficilmente richiederà un aumento spropositato del numero di transistor dedicati allo scopo.
La situazione è, insomma, relativamente stabile, com’è possibile visionare dalla sezione di decodifica dell’Athlon 64, ad esempio, che è ben visibile nonché dettagliatamente spiegata nell’immagine che si trova in questo link . Da notare che si tratta di una piccola parte dell’intero chip (in cui è evidente come la parte del leone la facciano i 512KB di cache L2), e a sua volta la funzione di decodifica è ridotta a pochi elementi che occupano anch’essi poco spazio.
Se consideriamo che il primo Athlon64 integra e ha il pieno supporto per le seguenti ISA:
- l’originale 8086
- l’80286 (modalità protetta a 16 bit)
- l’80386 e successive estensioni (80486, Pentium, e PentiumPro in primis)
- l’x86-64
e per le seguenti estensioni:
- MMX
- 3DNow!
- SSE 1 & 2
è comprensibile come il decoder, per quanto sicuramente molto complesso, non rappresenti più un problema di primaria e strategica importanza, anche nell’ottica delle future estensioni SSE5 da parte di AMD, e AVX da Intel (che ovviamente richiederanno un suo aggiornamento e ulteriore complicazione).
Lo è sicuramente su scale minori come Atom, ad esempio, che ha bisogno di contenere il numero dei transistor e il consumo, in quanto si colloca su una ben precisa nicchia di mercato. Non lo è su scale maggiori, dove il numero di transistor è decisamente più elevato e riservato a ben altre aree, com’è possibile in parte visionare da quest’altro link (in particolare per i chip di cui è disponibile uno spaccato particolareggiato delle varie sezioni).
Per quanto riguarda il numero di istruzioni, bisogna vedere in che modo vengano conteggiate. Penso che il migliaio lo si raggiunga sicuramente in termini di opcode conteggiati con relativi prefissi, ma non credo sia la strada giusta da seguire. E’ necessario tenere presente che i prefissi o parte dei bit dello stesso opcode “abilitano” o “disabilitano” particolari funzionalità, ma sempre della medesima istruzione.
Questo si verifica non soltanto per l’architettura x86, ma anche per tante altre. Per fare un esempio completamente diverso, l’istruzione LDM (Load Multiple registers) degli ARM prevede diverse modalità d’indirizzamento, nonché modalità di funzionamento e aggiornamento dei registri indirizzi, ma nell’elenco delle istruzioni ne risultano appena tre che fanno capo a essa.
Non v’è dubbio che il numero di istruzioni degli x86 sia in generale molto elevato rispetto alle altre CPU, ma sicuramente non siamo nell’ordine del migliaio (quelle legacy, poi, rappresentano una piccola parte). Inoltre c’è da aggiungere che la maggior parte ormai è legata alle estensioni SIMD, che rappresentano praticamente l’unica fonte di novità su questo fronte (molto difficilmente ne vengono introdotte all’ISA principale, e quando ciò avviene si tratta pochissime e magari singolari unità).
Infine sull’argomento supporto la situazione è effettivamente quella descritta: a conti fatti soltanto Intel viene supportata, perché ha in mano la maggioranza del mercato ed è l’unica che produce compilatori di qualità (ma supporta anche il GCC) che, per ovvie ragioni, funzionano bene o esclusivamente sui suoi prodotti. I concorrenti non hanno sufficiente know-how o risorse da dedicare allo scopo, e ciò inevitabilmente gli si ritorce contro.
L’unico spiraglio di concorrenza è rappresentato dalle estensioni relative alla virtualizzazione, visto che il supporto dev’essere garantito dai programmatori, che a questo punto però si ritrovano a dover scrivere codice diverso a seconda dell’ISA. In pratica l’onere è spostato sulle loro spalle, e ciò è sicuramente seccante.
In ogni caso, sia per le estensioni all’ISA che per quelle relative alla virtualizzazione, il risultato è che il codice prodotto, dovendo tenere conto di tutto ciò, occupa anche molto più spazio, poiché contiene i diversi code path che verranno selettivamente scelti al momento dell’esecuzione a seconda della CPU su cui gira il codice.
Secondo l’autore dell’articolo buona parte dei problemi esposti finora si risolverebbero se l’evoluzione dell’architettura fosse affidata a un collegio di aziende anziché all’arbitrarietà delle singole. Questo è in parte vero, perché eviterebbe sicuramente che ognuno reinventi la ruota (cioè istruzioni & opcode diversi per fare esattamente la stessa cosa) e costringa poi gli altri a supportarla, ma non sarebbe la soluzione definitiva, e questo per due motivi.
Il primo è che, anche se di “ruota” ce ne fosse soltanto una, la nuova richiederebbe comunque una soluzione ad hoc per poter essere sfruttata, che si dovrebbe necessariamente affiancare a quelle esistenti per le altre architetture. Quindi i code path continuerebbero a esserci in futuro, ma soltanto in numero limitato (uno in più alla volta, anziché n, uno per ogni azienda che ha tirato fuori la sua estensione proprietaria).
Il secondo è che in tutto ciò non si tiene conto dell’implementazione della medesima ISA, che può essere molto diversa e richiedere pertanto una soluzione ad hoc per essere adeguatamente sfruttata. Prendiamo ad esempio il Pentium 3, il 4 e il Pentium-M (che si sono susseguiti nel tempo): mettendo per il momento da parte le unità SIMD, si tratta di tre implementazioni diverse della stessa ISA, che richiedono codice diverso per ottenere le migliori prestazioni.
Questo lo si evince in particolare andando a leggere il manuale di Intel per le ottimizzazioni, che raccomanda anche soluzioni decisamente diverse a seconda della particolare implementazione. Lo stesso si potrebbe dire dei microprocessori di AMD, visto che fra K6-2/3, K7 e K8 ci sono delle differenze anche notevoli che richiedono code path specifici per ottenere le migliori prestazioni.
In definitiva il problema dei code path diversi continuerebbe comunque a esistere sia per le inevitabili estensioni (il mercato si evolve, e le ISA pure perché devono soddisfarne le esigenze) sia per le diverse implementazioni (tanto più se confrontiamo un Core Duo 2 con un Atom: le differenze sono abissali), ed è bene che i programmatori ne prendano atto…