Statistiche su x86 & x64 – parte 5 (indirizzamento verso la memoria)

Dopo aver discusso della distribuzione e frequenza del numero di operandi, questa volta puntiamo l’attenzione alle modalità d’indirizzamento verso la memoria che sono utilizzate dalle istruzioni dalle architetture x86 e x64.

E’ chiaro che non tutte le istruzioni hanno bisogno di accedere alla memoria, per cui le statistiche riguarderanno soltanto quelle che effettivamente ne faranno uso.

In particolare queste famiglie di microprocessori possono eseguire un solo accesso alla memoria, fatta eccezione per alcune particolari istruzioni “legacy” (quelle che operano sulle cosiddette “stringhe”), che consentono di specificare un operando sorgente (in lettura) e uno destinazione (in scrittura), e in un solo caso (CMPS) addirittura due operandi sorgente (entrambi in lettura).

Sebbene non strettamente legati alla lettura o scrittura di dati da e verso la memoria, sono stati inseriti anche i dati riguardo ai salti relativi.

La beta pubblica di Adobe Photoshop CS6 a 32 bit (PS32) mostra la seguente distribuzione:

Addressing modes:
  Address mode              Count
  [EBP-DISP8*4]            208695
  [REG+DISP8]              187256
  PC32                     151488
  [ESP+DISP8*4]            126268
  PC8                      100865
  [REG]                     66870
  [DISP32]                  42151
  [REG+DISP32]              41599
  [REG+REG*SC+DISP8]        30736
  [EBP-DISP32]              23608
  [REG+REG*SC]              20853
  [REG+REG*SC+DISP32]        2707
  [ESP+DISP32]                 78

I più attenti avranno notato subito una strana modalità d’indirizzamento che risulta del tutto inesistente in questa famiglia di processori, e che occupa già la prima posizione. Si tratta di quella con registro base EBP e offset negativo costituito da 8 bit, ma quest’ultimo risulta moltiplicato per quattro: [EBP-DISP8*4].

La ragione di questa scelta è racchiusa negli studi che sto affrontando da circa due anni a questa parte, dunque funzionali al progetto a cui sto lavorando, e di cui al momento non ho intenzione di discutere.

Questi dati, però, sono stati estratti dalle normali istruzioni x86, e dunque vanno collocati da qualche parte. Essi andranno a confluire nella classica modalità [REG+DISP8] e [REG+DISP32], che fanno uso rispettivamente di offset a 8 e 32 bit.

Simile alla precedente, e per la quale valgono le medesime considerazioni, è la modalità [ESP+DISP8*4], che utilizza il registro base ESP, ma questa volta con offset positivo e ancora una volta moltiplicato per quattro.

Com’è possibile vedere dai numeri, non si tratta di una suddivisione arbitraria, ma ben medita, poiché queste due modalità “fittizie” arrivano, assieme, alla somma di tutte le altre modalità d’indirizzamento basate su registro più offset.

La spiegazione è molto semplice, e dipende dall’ABI dell’architettura x86, che prevede un ampio uso dello stack sia per passare i parametri che per le variabili locali, e dunque la formazione di uno stack frame per il loro accesso, che si appoggia al registro EBP o ESP a seconda del tipo di variabile a cui accedere.

L’uso della moltiplicazione per 4 dell’offset deriva, invece, dall’analisi della dimensione tipica dei dati, pari a 4 byte (una double word), e tale risulta pure l’allineamento dell’offset.

Altro elemento importante da non sottovalutare, è il fatto che il registro EBP generalmente utilizza sempre un offset negativo, mentre per ESP è sempre positivo. Ciò è il motivo per cui gli offset sono sempre negativi e positivi rispettivamente, e dunque per massimizzare l’uso degli 8 bit a disposizione, questi vengono impiegati tutti allo scopo, forzando implicitamente il segno da utilizzare.

In ogni caso, e per ritornare più in linea col tema dell’articolo, il quadro che emerge è di un assoluto dominio della modalità d’indirizzamento data da un registro base più un offset rispetto agli altri, ossia quella ben più complicata che aggiunge anche un registro indice scalato ([REG+REG*SC+DISP]) e la più semplice con indirizzamento assoluto ([DISP32]).

Questi risultati non sorprendono, in quanto è ben più raro l’uso di un indice scalato, che in genere è presente quando si manipolano array; operazione, questa, non rara, ma che può incidere molto a livello prestazionale sui processori che non ne siano dotati (è facile che i loop manipolino degli array).

Un’altra doverosa precisazione va fatta per la modalità d’indirizzamento assoluto, [DISP32], che è stata riportata esclusivamente con offset a 32 bit, quando in realtà circa la metà delle istruzioni sfrutta un offset a 8 bit. Anche questa “anomalia” nasce dagli studi effettuati, che al momento non verranno discussi.

Infine e visto che ne sono stati riportati i dati, un accenno va alla modalità d’indirizzamento relativo, rappresentata da PC8 e PC32, che risulta molto frequente, a causa delle numerose istruzioni di salto (JMP, Jcc) e di chiamata a funzione (CALL), con le prime che prediligono offset a 8 bit, mentre le seconde a 32 bit.

Passando alla beta pubblica di Adobe Photoshop CS6 a 64 bit (PS64) lo scenario è, invece, questo:

Addressing modes:
  Address mode              Count
  [RSP+DISP8*8]            320474
  [REG+DISP8]              170953
  PC32                     168789
  PC8                      116645
  [REG+DISP32]              84105
  [REG]                     62352
  [RIP+DISP32]              58299
  [REG+REG*SC+DISP8]        49512
  [REG+REG*SC]              33659
  [RBP-DISP8*8]             25089
  [REG+REG*SC+DISP32]        5544
  [RSP+DISP32]               1643
  [DISP8]                       2

Non si presentano grosse sorprese, tenendo conto di quanto analizzato prima e delle differenze di ABI fra le due architetture, di cui abbiamo già ampiamente discusso nei precedenti articoli.

E’ quasi sparito l’uso della modalità d’indirizzamento che fa uso del registro RBP (EBP nel codice a 32 bit) con offset negativo, poiché non viene creato uno stack frame alla stessa maniera di x86, che si appoggia a questo registro per accedere alle variabili presenti nello stack.

In questo caso con x64 si fa semplicemente spazio nello stack per le variabili locali (incluse quelle usate per conservare temporaneamente i valori dei registri in cui sono stati passati i parametri), e quindi si usa esclusivamente RSP (ESP nel codice a 32 bit), accompagnato da un offset positivo, per indirizzarle.

Anche in questo caso si può notare subito il fatto che gli offset risultino moltiplicati per 8 (in precedenza per 4), per le stesse considerazioni fatte prima riguardo ai miei studi. Adesso, essendo i registri a 64 bit e non a 32, i dati sono, però, costituiti da 8 byte, appunto, e gli offset sono, quindi, allineati per questa quantità.

Per la cronaca, una scelta simile è stata fatta da Intel con l’architettura di Larrabee/Knights Corner (commercializzata come Xeon Phi), ma applicata soltanto all’accesso alla memoria da parte della nuova unità SIMD di cui è dotata.

La motivazione penso sia ormai chiara, ed è dettata dalla necessità di sfruttare al meglio gli 8 bit a disposizione, in modo da ottenere una migliore densità di codice. Infatti senza questa strategia di moltiplicare l’offset per la dimensione dei dati sarebbero stati richiesti molto più spesso offset a 32 bit, che occupano 3 byte in più. Cosa, questa, non da poco, in quanto stressa ancora di più la memoria e la cache TLB, incidendo, in ultima analisi, anche sulle prestazioni.

Tutto ciò risulta particolarmente evidente con x64, dove gli offset tendono a un naturale aumento a causa dei registri che occupano il doppio dello spazio, generalmente nell’uso degli interi e dei puntatori (entrambi a 64 bit, adesso). Anche le statistiche riflettono questo cambiamento, presentando un generale minor uso di offset a 8 bit, e un corrispondente aumento di quelli a 32 bit, com’è possibile notare comparando le rispettive modalità in x86 e x64.

Per quest’ultima architettura si registra, invece, un generale aumento delle modalità che fanno uso, oltre al registro base e all’offset, di un registro indice scalato: circa il 50% in più rispetto a x86, con punte del 100% quando si utilizzano offset a 32 bit. Non sono chiare le motivazioni che hanno portato a questi risultati, per far luce sui quali sarebbe necessario andare ad analizzare i contesti in cui sono state usate nelle istruzioni, ma ciò esula dallo scopo di questa serie di articoli (lo sforzo richiesto, peraltro, sarebbe notevole).

Risulta, poi, quasi sparita la modalità con indirizzamento assoluto, [DISP32], che è stata rimpiazzata da [DISP8] e [RIP+DISP32]. [DISP8] in questo caso viene usata per accedere a variabili di thread (o di kernel, nel caso di codice del s.o.), come vedremo nel pezzo di chiusura della serie, quando discuteremo dei numeri sul “legacy“.

[RIP+DISP32] viene, invece, impiegato per accedere alle variabili globali. Il risultato è simile a x86 (funzionalmente è identico all’uso di [DISP32]), ma con la notevole differenza che in questo caso il codice è relativo (rispetto al program counter: RIP) e non assoluto com’era in precedenza (e che richiede eventualmente un’operazione di rilocazione da parte del loader del s.o. quando carica l’eseguibile).

E’ singolare, invece, che l’uso di [RIP+DISP32] sia aumentato del 38% circa rispetto [DISP32], ma facilmente spiegabile con la diversa ABI adottata in x64 rispetto a x86, con la prima che fa un maggior uso di istruzioni LEA per caricare nei registri i puntatori da passare alle routine da chiamare, anziché istruzioni PUSH con valori immediati (a 32 bit) per lo stesso scopo.

L’uso di PC8 e PC32, infine, è paragonabile a quello di x86, con un leggero aumento di circa il 10% per entrambi.

Il prossimo articolo della serie si focalizzerà sulla distribuzione dei valori immediati (interi) utilizzati in alcune istruzioni.

Press ESC to close