Torniamo a questa serie di articoli, che è rimasta da troppo tempo nel cassetto, ma che merita di essere conclusa con le ultime parti. In questo, in particolare, analizzeremo le statistiche relative al numero degli argomenti di un’istruzione, che sono messi in relazione con le varie modalità d’indirizzamento.
Come sappiamo, ogni istruzione può avere un numero variabile di argomenti, da nessuno fino a un massimo di 4 (per alcune istruzioni AVX; l’ultima estensione SIMD di Intel). Ogni argomento non può essere di qualunque tipo, ma esistono delle regole ben precise. Ad esempio può esserci un solo valore immediato, se è presente. Oppure può esserci un solo operando che punta a una locazione di memoria.
In realtà esistono delle pregevoli eccezioni, che sono rappresentate dalle istruzioni cosiddette di “stringa” (di cui abbiamo discusso nella serie di articoli sull’aspetto “legacy” di x86), che possono anche operare contemporaneamente con due locazioni di memoria, oppure con una locazione di memoria e una porta di I/O (che, in realtà, rimane pur sempre una memoria esterna al processore).
La libreria (diStorm3) per disassemblare codice x86/x64 che ho usato per generare le statistiche restituisce in questi casi due argomenti per le istruzioni, e nel caso di un riferimento alla memoria riporta il classico indirizzamento indicizzato, facendo uso del registro SI e/o DI.
In realtà è bene precisare che queste istruzioni sono riportate senza operandi nei manuali di Intel e AMD, in quanto il meccanismo di utilizzo dei registri e dell’indirizzamento della memoria è implicito nella medesima istruzione.
Quella operata da diStorm3 è, pertanto, una scelta arbitraria, ma risulta comunque comoda perché fa capire che per quelle istruzioni viene in ogni caso eseguito un accesso in memoria con una determinata modalità.
Ciò detto, analizziamo la distribuzione delle varie combinazioni di argomenti riscontrati per x86, ricorrendo, al solito, alla beta pubblica di Adobe Photoshop CS6 a 32 bit (PS32):
Operands Count REG,[REG+DISP] 379139 REG 297616 PC 252353 REG,REG 198565 [REG+DISP],REG 145407 REG,IMM 99129 IMM 80916 [REG+DISP],IMM 71467 [REG+DISP] 57483 REG,[REG+REG*SC+DISP] 36837 FPU 17887 REG,[DISP] 16086 [DISP],REG 15417 FPU,FPU 11872 [REG+REG*SC+DISP] 10322 [DISP] 8928 [REG+REG*SC+DISP],REG 5537 HREG,IMM 3661 [DISP],IMM 1634
Ero indeciso se riportare meno dati per non allungare troppo la tabella, ma poi ho preferito prendere i primi 20 anziché i primi 10 o 15, perché emerge qualche dato interessante fra gli ultimi, non tanto per la frequenza, che ovviamente risulta piuttosto bassa (basti vedere i numeri dei primi 10), quanto per la tipologia degli argomenti.
Leggendo il primo numero si potrebbe arrivare alla classica conclusione che… ci troviamo di fronte a un CISC. Infatti abbondano i riferimenti alla memoria, e questo si verifica anche in altri casi.
In realtà quando analizzeremo l’elenco delle istruzioni (sotto forma di mnemonici), nel prossimo articolo della serie, vedremo che la situazione non è esattamente come potrebbe sembrare a primo acchito, guardando soltanto alla tipologia degli operandi. Un po’ di pazienza, pertanto, e il quadro sarà molto più chiaro.
Dai primi 5 risultati, che dominano l’intero panorama, emerge un quadro abbastanza chiaro. Innanzitutto le operazioni che leggono dalla memoria (prima posizione) sono più del doppio rispetto a quelle che vi scrivono (quinta posizione).
In mezzo troviamo operazioni coi registri, rispettivamente con uno solo (seconda posizione), presumibilmente a causa dell’ABI di x86, che fa abbondante uso di PUSH e POP dei registri (ma anche l’uso di un valore immediato, IMM, che è in settima posizione, è spiegabile con PUSH di valori immediati), e poi con due registri (quarta posizione).
In terza posizione si collocano le istruzioni che fanno uso di salti relativi, che vedremo essere incarnati dalle istruzioni CALL, JMP, e Jcc (salto su condizione).
Gettando lo sguardo un po’ più in basso, appare finalmente qualche istruzione/modalità squisitamente CISC. Mi riferisco in particolare a [REG+DISP],IMM, il cui scopo è utilizzare un valore immediato (come sorgente) e in una locazione di memoria (in genere come seconda sorgente, ma anche come destinazione).
Infatti ciò non trova riscontro in una macchina RISC, in quanto per realizzare la medesima operazione sarebbe necessaria almeno un’istruzione per caricare il valore immediato in un registro (ma soltanto nei casi più semplici, con valori immediati piccoli o comunque appartenenti a un insieme ristretto), e un’altra per memorizzarlo successivamente in memoria. Questo nel caso più semplice di una semplice MOVE; con istruzioni più complesse (ad esempio una ADD) servirebbe anche una lettura dell’operando dalla memoria.
Il tutto, quindi, sporcando un registro e aggiungendo una dipendenza nella pipeline. Questo perché la store dipende dalla precedente MOVE col valore immediato. Esistono, comunque, tecniche di propagazione del valore che potrebbero evitare questa dipendenza.
Tornando alle statistiche, più rare sono le modalità d’indirizzamento complesse, come [REG+REG*SC+DISP], che non tutti i RISC supportano proprio a motivo della loro complessità, ma questo era già emerso dalla precedente analisi incentrata sulle singole modalità d’indirizzamento della memoria.
E’ interessante, invece, notare che queste riguardano per lo più la lettura verso la memoria, mentre la scritture risultano di gran lunga meno frequenti, come pure l’uso in istruzioni che adoperano un solo argomento di questo tipo.
Molto rara, ma pur sempre presente, è l’uso dell’FPU, in particolare con un solo registro; oppure, un po’ meno frequente, con una coppia di registri dell’unità x87.
Singolare, invece, è l’accesso diretto alla memoria, quindi senza far uso di registri, che è rappresentato dall’etichetta [DISP]. Si potrebbe pensare a un eccessivo utilizzo di variabili globali (che nel buon codice dovrebbero essere usate il meno possibile), ma anche qui il motivo principale verrà fuori nell’ultimo articolo della serie, quando andremo a vedere i numeri degli aspetti “legacy” di quest’architettura.
Verso la fine salta fuori un altro aspetto “legacy” di x86, che riguarda la presenza di registri “alti” (AH, BH, ecc.) a 8 bit, che sono stati etichettati come HREG. Il loro uso è sporadico, perché si tratta di un’ottimizzazione (la possibilità di accedere agli 8 bit alti dei primi 4 registri general purpose) ormai in disuso, anche perché potrebbe creare problemi con l’implementazione (l’accesso parziale a un registro).
Arriva finalmente il momento di dare un’occhiata ai numeri di x64, grazie alla beta pubblica di Adobe Photoshop CS6 a 64 bit (PS64):
Operands Count REG,[REG+DISP] 427264 REG,REG 313662 PC 285434 [REG+DISP],REG 192774 REG,IMM 119497 REG 105832 REG,[REG+REG*SC+DISP] 55623 [REG+DISP],IMM 49858 SSE,SSE 40482 SSE,[REG+DISP] 22811 [REG+DISP] 17786 [REG+DISP],SSE 12397 [REG+REG*SC+DISP],REG 11685 SSE,[REG+REG*SC+DISP] 8711 [REG+REG*SC+DISP],SSE 5982 [REG+REG*SC+DISP] 4221 [REG+REG*SC+DISP],IMM 2493 REG,REG,IMM 2163 SSE,REG 2162
La lettura delle prime 6 posizioni va fatta sempre in ottica dell’ABI, che è cambiata rispetto a x86. Adesso sono disponibili molti più registri, per cui sono richiesti meno caricamenti di dati dalla memoria, e quind si fa più uso dei registri (ecco perché REG,REG è balzato al secondo posto). Inoltre le PUSH e le POP sono di gran lunga inferiori, e ciò spiega il netto calo dell’uso di un solo registro.
Non bisogna, però, farsi ingannare dal fatto che la prima posizione, occupata da REG,[REG+DISP], implichi un maggior numero di caricamenti. In realtà in questa tipologia di argomenti ricade l’uso della famigerata istruzione LEA, che serve a caricare in un registro un indirizzo di memoria.
Poiché l’ABI x64 è pesantemente basata sull’uso dei registri come parametri delle funzioni da richiamare, si piega il perché dell’aumento di REG,[REG+DISP] rispetto a x86, come pure la completa scomparsa del singolo argomento dotato di valore immediato (IMM, usato per il push di valori immediati sullo stack in x86).
Un altro dato interessante che salta subito all’occhio è il massiccio uso dell’indirizzamento della memoria con la modalità più complessa, cioè [REG+REG*SC+DISP], che è presente in tutte le salse (contribuisce, anche qui, la diversa ABI): dall’uso coi registri general purpose, a quelli dell’unità SIMD SSE.
Infatti si nota immediatamente come le SSE facciano la parte del leone, con numeri abbastanza consistenti, se paragonati all’uso dell’FPU per il codice x86. Ovviamente le istruzioni più frequenti sono quelle che fanno esclusivamente uso di registri SSE (SSE,SSE).
Altre rilevanti differenze con x86 si notano nell’assenza dell’accesso diretto alla memoria (etichettato con [DISP]), come pure la mancanza di utilizzo di registri “alti” (AH, BH, ecc.), chiaro segno di un minor ricorso alle caratteristiche “legacy“.
Saltano fuori, infine, istruzioni con tre operandi (REG,REG,IMM, utilizzato dalla relativa istruzione IMUL), e altre che servono a copiare il valore di un registro general purpose in uno SSE (SSE,REG). Si tratta di istruzioni rare, ma che sono riuscite a rientrare nelle prime 20 posizioni di quelle più frequenti.
Con ciò si conclude quest’analisi. Nel prossimo articolo della serie, come già anticipato, punteremo lo sguardo sulla frequenza delle istruzioni / mnemonici.