Statistiche su x86 & x64 – parte 1 (macrofamiglie di istruzioni)

La lunga serie sull’analisi dell’aspetto legacy” di x86 e x64 si era conclusa con l’annuncio (nei commenti) di una nuova serie di articoli che avrebbero trattato queste due ISA dal punto di statistico; è arrivato il momento di mantenere fede alle promesse.

Esistono già numerosi articoli e pubblicazioni scientifiche che hanno affrontato l’argomento, ma quelli che seguiranno sono frutto di particolari studi effettuati su diversi aspetti delle istruzioni di queste architetture dal punto di vista quantitativo, ma che offrono spazio anche ad alcune riflessioni.

Parlo non a caso di architetture, distinguendo quindi nettamente x86 e x64, perché, pur essendo quest’ultima un’estensione della prima, le differenze sono notevoli non soltanto dal punto di vista dell’ISA (x64 ha raddoppiato il numero di registri, eliminato alcune istruzioni legacy, e introdotto una nuova modalità d’indirizzamento che fa uso dell’IP / PC), ma anche riguardo alla tipologia di codice effettivamente eseguito per svolgere gli stessi compiti.

In realtà è bene precisare che il codice prodotto ed eseguito dipende molto anche dall’ABI utilizzata, quindi dall’uso dei registri “imposto” dal sistema operativo su cui sta girando, e in ultima analisi anche da quello del compilatore, che nella produzione di codice può, ad esempio, riservare alcuni registri per particolari funzionalità.

Non ci occuperemo di questi due aspetti, se non tenendo conto che gli eseguibili utilizzati per estrapolare le informazioni sono stati generati per il s.o. Windows nelle incarnazioni a 32 (x86) e 64 (x64) bit, e dunque trattasi di file EXE o DLL che utilizzano il famoso formato PE che trae origini dal vecchio DOS, ma che col tempo è stato esteso per supportare anche architetture ben diverse da quelle di Intel e AMD, come pure dell’IL impiegato in .NET.

In particolare sono stati utilizzati gli eseguibili della beta pubblica di Adobe Photoshop CS6, che nell’archivio si trovano come file senza estensione nominati _75_777015c53e2f5a702d01589cd0020445 e _73_7d4efa378b68bcee72e36ede90f7907d, rispettivamente per la versione a 32 e 64 bit dell’applicazione, che successivamente verranno indicati come PS32 e PS64.

Gli studi hanno coinvolto l’analisi anche di diversi altri eseguibili (ad esempio il MAME, Firebird SQL, MySQL, VirtualBox, ecc., come pure il driver OpenGL di AMD/Ati), ma questi due sono stati privilegiati perché hanno consentito di estrapolare molte più istruzioni, consentendo, quindi, di avere delle statistiche molte più estese.

Per disassemblare gli eseguibili, per poi raccoglierne tutte le statistiche, è stata utilizza diStorm3, una comoda libreria, di cui esistono binding per Python, che mette a disposizione nella sezione download un’applicazione (dislib.py) Python per disassemblare proprio gli eseguibili PE.

dislib.py si limita a disassemblare al massimo 4KB di codice partendo dall’entry point (indirizzo della prima istruzione eseguita), per cui è stata pesantemente modificata per arrivare a coprire / scovare quanto più codice possibile, senza però andare a disassemblare ciecamente zone del file che potrebbero essere dati, i quali avrebbero falsato le informazioni raccolte.

In questo modo si è ottenuto un piccolo insieme di tutte le istruzioni presenti nell’eseguibile, ma di buona qualità (niente spazzatura, in sostanza) per gli scopi che erano stati fissati, e cioè raccogliere dati utili a comprendere la natura e la conformazione delle varie istruzioni, a cui si aggiungono altre finalità che non riguardano squisitamente le statistiche.

Sono state analizzate circa 1,75 milioni di istruzioni (1.746.569 per la precisione) per PS32, e circa 1,74 milioni (1.737.331) per PS64. Non sono, per ovvie ragioni, un campione rappresentativo dell’intera tipologia di codice e istruzioni che si trovano nelle più disparate applicazioni o giochi, ma vengono prese esclusivamente per presentare dei dati (da qualcosa si dovrà pur partire) su cosa si può trovare in un eseguibile x86 o x64.

Nel primo caso lo spazio occupato dalle istruzioni è pari a circa 5,63MB (5.634.556 byte esattamente), mentre nel secondo è di 7,56MB (7.556.180 byte). Si tratta del primo dato interessante poiché, assieme al numero di istruzioni, ci fornisce una misura della dimensione media del codice, che è pari a circa 3,2 byte e 4,3 byte, rispettivamente, per istruzione.

Il codice a 32 bit appare, dunque, generalmente più denso rispetto a quello a 64 bit, che risulta penalizzato in qualche misura. Ciò dipende da due fattori, essenzialmente: l’uso del prefisso REX (usato per accedere ai nuovi 8 registri che sono stati aggiunti all’ISA, come pure per specificare che si stanno manipolando 64 bit e non 32) e la diversa ABI adottata (nel codice a 32 bit si esegue il push sullo stack dei parametri da passare a una funzione, mentre in quello a 64 bit si caricano valori sui registri), come vedremo in un prossimo articolo che metterà anche a confronto alcuni spezzoni di codice.

Le istruzioni sono state poi divise in macrofamiglie: INTEGER, FPU, MMX, 3DNOW, SSE, AVX, che indicano a quale unità funzionale appartengono con l’eccezione di INTEGER, che non si riferisce soltanto all’ALU per i calcoli interi, ma che si fa carico di tutte le istruzioni che appartengono al “core” del processore (quindi anche di salti, segmenti, istruzioni legacy varie, virtual machine, ecc.).

A queste è stata aggiunta anche AVXFAKE, che serve esclusivamente a comprendere quanto spazio occuperebbe un’istruzione SSE se fosse codificata “così com’è” nel nuovo set di istruzioni AVX che è stato recentemente aggiunto da Intel alla sua famiglia di processori, con lo scopo non soltanto di estendere il set d’istruzioni (ad esempio dando la possibilità di specificare un terzo o quarto registro con cui operare), ma di ottenere anche una (de)codifica più semplice e, in ultima istanza, compatta (ma solo con codice appositamente generato, che è in grado di sfruttarne meglio le caratteristiche).

Di seguito sono riportati i risultati per PS32:

Class        Count       % Avg sz
INTEGER    1631136   93.39    3.2
FPU         114521    6.56    3.2
SSE            912    0.05    4.0
AVXFAKE        912    0.05    5.0

e per PS64:

Class        Count       % Avg sz
INTEGER    1638505   94.31    4.3
SSE          93942    5.41    5.2
AVXFAKE      93942    5.41    5.3
FPU           4884    0.28    3.1

Com’è possibile notare, la stragrande maggioranza è costituita da istruzioni che ricadono nel dominio del “core” della CPU, e che quindi alla fine determina anche la densità dell’intero codice.

Ciò, però, non ci consente di esprimere valutazioni sulle prestazioni, che sono determinate mediamente per il 90% dai cicli eseguiti, e in questi cicli si può fare pesante uso di istruzioni SSE, tanto per fare un esempio. Inoltre è bene ricordare che quello utilizzato è soltanto un campione che non è rappresentativo né della totalità delle tipologie di codice né dell’intera applicazione a cui appartengono, come già detto.

Un altro dato che emerge è che nel codice a 32 bit sono presenti poche istruzioni SSE, mentre molte sono quelle che appartengono alla vecchia FPU x87. Viceversa, il codice a 64 bit predilige le SSE a discapito dell’FPU. Il tutto sempre tenendo conto dei limiti dell’analisi esposti.

Infine un commento lo merita anche il confronto fra il codice SSE e l’equivalente AVXFAKE, visto che è stato introdotto appositamente. Nel caso di codice a 32 bit le istruzioni SSE presentano una densità di codice migliore rispetto alla codifica AVX, mentre per il codice a 64 bit la densità è sostanzialmente equivalente.

Il motivo è presto spiegato: nel codice a 64 bit si fa uso del famigerato prefisso REX per accedere ai nuovi 8 registri SSE, mentre con la codifica AVX ciò non è necessario nella maggior parte dei casi, in quanto una buona parte delle funzionalità di REX sono assorbite / integrate nei prefissi utilizzati da AVX, oltre al fatto che AVX integra pure i prefissi 66, F2 e F3, come già spiegato in un precedente articolo.

Considerato che il codice a 64 bit è destinato a dominare nel futuro, e che AVX porta con sé altre innovazioni (come ad esempio istruzioni con 3 o 4 operandi, oppure le nuove istruzioni di gather/scatter), risulta evidente che il codice AVX risulterà mediamente più denso rispetto al vecchio SSE.

Nel prossimo articolo continueremo l’analisi mostrando la distribuzione delle istruzioni in base alla loro dimensione, e poi in base al numero di operandi.

Press ESC to close