Statistiche su x86 & x64 – parte 9 (legacy e conclusioni)

Dopo aver analizzato le statistiche sugli mnemonici arriviamo finalmente alla conclusione di questa serie, trattando un argomento che è molto caro all’architettura x86 e x64, ossia il cosiddetto “legacy“.

Per comodità riporto i link a tutti i precedenti articoli:

parte 1 (macrofamiglie di istruzioni)
parte 2 (distribuzione per dimensione)
parte 3 (ISA / istruzioni a confronto)
parte 4 (numero di operandi)
parte 5 (indirizzamento verso la memoria)
parte 6 (valori immediati)
parte 7 (operandi)
parte 8 (istruzioni / mnemonici)

Visto che parliamo di numeri, è naturale chiedersi se e quanto viene ancora usato degli aspetti più complessi e vecchi che questa famiglia di microprocessori si trascina dietro ormai da diverse decadi (essendo una delle prime che è riuscita a sopravvivere fino ai giorni nostri).

Argomento, questo, che è stato ampiamente discusso in passato in un’apposita serie di articoli, a cui rimando per meglio comprendere ciò di cui si sta parlando se non risultasse immediatamente chiaro.

Al solito, per ricavare i dati da analizzare faremo uso della beta pubblica di Adobe Photoshop CS6 a 32 bit (PS32), ma le istruzioni legacy verranno raggruppate per “tipologia” (FPU, stringhe, lock, vecchie, complesse):

Mnemonic              Count      % Avg sz
FNSTSW                 4220   0.24    2.0
FLDCW                   138   0.01    3.2
FNSTCW                   69   0.00    3.2

REP MOVS                498   0.03    2.0
MOVS                     58   0.00    1.9
REP STOS                 51   0.00    2.2
STOS                      1   0.00    2.0

LOCK XADD              1352   0.08    4.0
LOCK BTS                177   0.01    5.0
LOCK CMPXCHG             16   0.00    4.0
LOCK XCHG                 1   0.00    3.0

CDQ                    1080   0.06    1.0
SAHF                    558   0.03    1.0
CWDE                    347   0.02    1.0
INT 3                   250   0.01    1.0
SALC                      8   0.00    1.0
XLAT                      3   0.00    1.0
CBW                       2   0.00    2.0
DAA                       2   0.00    1.0
CMC                       1   0.00    1.0
AAD                       1   0.00    2.0

SHLD                     42   0.00    4.0
LEAVE                    25   0.00    1.0
SHRD                     17   0.00    4.0
LOOP                      1   0.00    2.0
LOOPNZ                    1   0.00    2.0

Preciso che ho voluto creare le ultime due categorie per meglio evidenziare alcune differenze, e che la scelta fatta, di conseguenza, è soltanto mia e “arbitraria”.

L’FPU x87 è spesso considerata in toto come “legacy” e molto è stato detto in proposito, ma in questo contesto ho voluto riportare i dati esclusivamente su alcune istruzioni “esotiche”, che operano sulla status word e sulla control word di quest’unità d’esecuzione.

Da roba legacy ci si aspetterebbe un uso estremamente ridotto, come peraltro dimostra l’andamento generale, di questo genere di istruzioni. Per questo può inizialmente sorprendere l’elevata frequenza di FNSTSW, ma andando a leggere l’articolo sul legacy che tratta appositamente dell’FPU tutto diventa molto più chiaro, così come risulta chiara la frequenza non indifferente dell’istruzione SAHF (presente nel terzo gruppo).

Passando alle istruzioni di stringa, salta subito all’occhio la generosa presenza della REP MOVS, il cui scopo è quello di copiare porzioni di memoria. Essendo questa un’operazione relativamente frequente nel codice, si comprende il perché del suo impiego.

Apprezzabile è anche l’uso delle istruzioni LOCK XADD e LOCK BTS, che servono a modificare atomicamente una certa locazione di memoria, rendendo molto più semplice ed efficiente l’implementazione di semafori e mutex. Strumenti, questi, impiegati da Photoshop, che è un’applicazione multithread progettata per sfruttare i core che il processore mette a disposizione, da cui l’elevata frequenza di queste istruzioni che fanno uso de vetusto prefisso LOCK.

Fra le istruzioni più vecchie spicca moltissimo CDQ, che serve a estendere (con segno) un intero a 32 bit in uno a 64 bit, ma contenuto nella coppia di registri EAX ed EDX. Difficile pensare a un impiego di questa istruzione, se non per ricavare il segno dell’intero a 32 bit (contenuto in EAX).

CWDE, che estende con segno il registro AX (16 bit) a 32 bit (EAX) ha una frequenza ben più bassa, ma che trova poca giustificazione, considerato il fatto che con le architetture a 32 bit (quindi non soltanto x86) l’uso di interi a 16 bit (word) risulta molto ridotto (i tipi di dato più frequenti sono l’intero a 32 bit e quello a 8 bit).

INT 3, che ha discreta frequenza, potrebbe essere impiegata così spesso per effettuare chiamate al sistema operativo per alcuni servizi che magari espone con questo meccanismo.

Ben più rare risultano, poi, le istruzioni che eseguono shift combinando due registri (SHLD SHRD), e lo stesso vale per l’istruzione di rimozione dello stack frame (LEAVE), che però non incontra nel codice la sua duale che dovrebbe essere eseguita all’inizio (la famigerata ENTER), ma che generalmente viene simulata eseguendo apposite istruzioni.

Anche il codice x64 presenta per grandi linee un andamento simile (per quanto riguarda la bassa frequenza) a quello di x86, come viene fuori dall’analisi dei dati estratti grazie alla beta pubblica di Adobe Photoshop CS6 a 64 bit (PS64):

Mnemonic              Count      % Avg sz
REPNZ SCAS              434   0.02    2.2
REP STOS                 28   0.00    2.6
REP CMPS                 19   0.00    2.1
REP MOVS                  2   0.00    2.0

LOCK DEC                795   0.05    4.0
LOCK XADD               503   0.03    4.8
LOCK BTS                203   0.01    5.0
LOCK INC                 81   0.00    4.0
LOCK CMPXCHG             21   0.00    5.4

INT 3                  1073   0.06    1.0
CDQ                     914   0.05    1.0
CDQE                    599   0.03    2.0
CQO                     291   0.02    2.0
CWDE                     44   0.00    1.0

E’ palese la completa assenza di istruzioni legacy dell’FPU, ma il risultato era decisamente scontato, considerato che il codice x64 predilige fortemente l’uso dell’unità SIMD.

Risulta decisamente anomalo e incomprensibile il quadro che viene fuori per le operazioni “di stringa”, dov’è sputata una REPNZ SCAS che si è piazzata in cima al gruppetto, relegando alla fine proprio la ben più comune REP MOVS.

Leggermente diversa è, invece, la situazione delle istruzioni che fanno uso del prefisso LOCK, dove adesso sono presenti le istruzioni LOCK DEC e LOCK INC (sempre per implementare i semafori). Ciò che sorprende, però, è l’asimmetria fra i decrementi e gli incrementi della variabile (usata per tenere conto delle risorse a disposizione, e dello stato di attesa), con una differenza di quasi un ordine di grandezza.

Infine il gruppetto delle istruzioni “vecchie” ha visto un deciso dimagrimento, dovuto anche al fatto che su x64 alcune di esse sono state rimosse dall’ISA. E’ rimasta la INT 3, probabilmente per le motivazioni già esposte, e quelle di estensione con segno del registro EAX/RAX.

E’ sparito del tutto il gruppo delle istruzioni complesse, sia per l’ABI (x64, come già spiegato nei precedenti pezzi, non crea uno stack frame all’inizio di ogni routine, e dunque non serve l’istruzione LEAVE) sia per il fatto che erano già abbastanza rare anche su x86.

Ci sono ancora un altro paio di aspetti del legacy che emergono dall’analisi dei dati raccolti, sia per x86 che per x64. Il primo riguarda la famigerata segmentazione (ricordiamo che i segmenti sono poi diventati selettori nella modalità protetta), che è stata spesso usata per mettere in croce questa famiglia di processori.

Su x86 la situazione è la seguente:

Segments:
  Seg      Count
   FS      18108

Quindi i segmenti (selettori) vengono ancora usati, e hanno un loro peso (circa l’1%) che non è certo trascurabile. Le motivazioni sono state esposte dettagliatamente verso la fine dell’apposito articolo, e riguardano il fatto che i moderni s.o. ormai usano esclusivamente la paginazione, relegando la segmentazione soltanto all’accesso veloce alle variabili di thread o a quelle del kernel.

Su x64, invece, il quadro è completamente diverso:

Segments:
  Seg      Count
   GS          2

Uno scenario molto simile si è presentato anche analizzando i dati dell’eseguibile di Firebird SQL, e dunque non risulta anomalo di per sé, ma che sembra, quindi, un pattern che si ripete passando da x86 a x64. Per capire il perché ci sia un così drastico calo dell’uso dei selettori servirebbe magari un’analisi dell’esecuzione del codice a runtime, ma questo esula dallo scopo dell’articolo.

Un ultimo aspetto legacy riguarda, infine, l’uso dei registri “alti” (AH, BH, CH e DH), che in parte è venuto fuori dall’analisi degli operandi.

Su x86 i numeri ci dicono questo:

Operands:                                     Count
HREG,IMM                                       3661
[REG+DISP],HREG                                  13
REG,HREG                                          5
HREG,HREG                                         4
HREG,[REG+DISP]                                   3
HREG,REG                                          1

Com’è lecito aspettarci, in generale si fa pochissimo uso di questa vecchissima funzionalità, anche se il caso che si presenta in cima alla lista non è certo poco comune.

Ciò che sorprende è, invece, il fatto che non ve n’è alcuna traccia su x64, e ciò non perché sia stata rimossa dall’ISA questa possibilità. Sembra, dunque, che i compilatori preferiscano generare codice abbastanza diverso per questa nuova incarnazione a 64 bit di x86.

Di fatti, e per concludere, la tendenza che si riscontra sia su x86, ma maggiormente con x64, è che l’uso di istruzioni e funzionalità legacy è decisamente calato rispetto al passato, con un approccio molto più orientato alla filosofia RISC, che privilegia la semplicità alla complessità (anche se non è del tutto vero, visto che esistono RISC abbastanza complessi).

Alcuni aspetti legacy rimangono, e possono anche portare contributi significativi sia alla compattezza che alla velocità del codice. Per questo non bisogna farsi ingannare dalla frequenza delle istruzioni, che certamente non può anche essere un indice della frequenza con la quale il processore eseguirà una particolare istruzione.

Un chiaro esempio è l’istruzione REP MOVS che, sebbene rara, potrebbe essere utilizza internamente alla routine memcpy (se il compilatore non ne prevede l’inlining per velocizzare l’esecuzione), la quale può essere invocata anche parecchie volte all’interno del codice.

Per chiudere, è utile sottolineare che anche l’uso di costanti immediate e di offset per l’indirizzamento della memoria da parte di alcune istruzioni è decisamente significativo.

Non si tratta di un aspetto tipicamente CISC, visto che anche sui RISC è possibile specificare valori immediati nelle istruzioni, come pure offset da sommare ai registri quando s’indirizza la memoria, ma è certamente vero che, avendo questi ultimi opcode a lunghezza fissa, hanno a disposizione un range molto limitato di valori dai quali è possibile attingere, mentre sui primi c’è una ben più ampia possibilità (anche con valori a 64 bit) che viene effettivamente sfruttata.

Si tratta di una caratteristica che non va sottovalutata e che, a mio modesto avviso, rappresenta ancora una valida carta che la macrofamiglia dei processori CISC può giocarsi nei confronti di quella RISC.

Con questo articolo si completa la serie di articoli che hanno analizzato le architetture x86 e x64 dal punto di vista puramente statistico (e con tutti i limiti del caso).

P.S. L’articolo è stato completato il 14, ma è stato pubblicato oggi per esigenze di redazione (causa attuale periodo di ferie).

Press ESC to close