2011-02-05 12 views
17

Sto lavorando con Kip Irvine's "Assembly Language per x86 Processors, sesta edizione" e mi sto davvero divertendo.Perché il codice deve essere allineato ai limiti di indirizzo pari su x86?

Ho appena letto la mnemonico NOP nel paragrafo seguente:

"It [NOP] is sometimes used by compilers and assemblers to align code to 
even-address boundaries." 

L'esempio dato è:

00000000 66 8B C3 mov ax, bx 
00000003 90   nop 
00000004 8B D1  mov edx, ecx 

Il libro afferma poi:

"x86 processors are designed to load code and data more quickly from even 
doubleword addresses." 

La mia domanda is: È la ragione per cui è così perché, per i processori x86, il libro si riferisce a (32 bit), la dimensione della parola di la CPU è a 32 bit e quindi può tirare le istruzioni con il NOP e elaborarle in una volta sola? Se questo è il caso, sto assumendo che un processore a 64 bit con una word size di una quadword farebbe questo con un ipotetico 5 byte di codice più un nop?

Infine, dopo aver scritto il mio codice, dovrei passare e correggere l'allineamento con NOP per ottimizzarlo, o il compilatore (MASM, nel mio caso), farebbe questo per me, come sembra suggerire il testo?

Grazie,

Scott

+7

Tutto quello che vuoi sapere sull'architettura dei processori moderni è su http://www.agner.org/optimize/. L'allineamento richiesto per le istruzioni è indipendente dalla dimensione della parola ed è 16 byte per i moderni processori Intel.Non voglio rovinarti il ​​divertimento, ma non devi fidarti di un libro che fa affermazioni generiche sulle prestazioni di "processori x86". Ogni singolo modello ha caratteristiche diverse. –

+0

Grazie per il tuo commento! Non hai rovinato il mio divertimento - la gioia è nell'apprendimento e ho appena imparato un po 'di più da te! Controllerà anche il sito web. –

+0

Questo libro sembra orribilmente obsoleto. 16bit x86 è molto antico, TBH Non vedo il valore nell'insegnare questa roba anche a scopo didattico. Forse come esempio contrario come _non_ progettare un linguaggio processore/assembly. – hirschhornsalz

risposta

17

codice che viene eseguita sulla parola (per 8086) o DWORD (80386 e successivi) confini esegue più veloce perché il processore recupera interi (D) parole. Quindi se le tue istruzioni non sono allineate, allora c'è uno stallo durante il caricamento.

Tuttavia, non è possibile allineare a dword tutte le istruzioni. Beh, immagino che potresti, ma poi sprechi spazio e il processore dovrebbe eseguire le istruzioni NOP, il che ucciderebbe qualsiasi beneficio in termini di prestazioni nell'allineare le istruzioni. In pratica, l'allineamento del codice su dword (o qualsiasi altra cosa) dei limiti aiuta solo quando l'istruzione è il bersaglio di un'istruzione branching, ei compilatori tipicamente allineeranno la prima istruzione di una funzione, ma non allineeranno i target di ramo che possono raggiungibile anche attraverso la caduta. Per esempio:

MyFunction: 
    cmp ax, bx 
    jnz NotEqual 
    ; ... some code here 
NotEqual: 
    ; ... more stuff here 

Un compilatore che genera il codice in genere allineare MyFunction perché è una destinazione del ramo (raggiungibile con call), ma non sarà allineare il NotEqual perché così facendo avrebbe inserire NOP istruzioni che avrebbe essere giustiziato quando cade. Ciò aumenta le dimensioni del codice e rallenta il caso di fall-through.

Suggerirei che se si sta solo imparando il linguaggio assembly, non ci si deve preoccupare di cose come questa che spesso ti danno guadagni marginali delle prestazioni. Basta scrivere il tuo codice per far funzionare le cose. Dopo aver lavorato, puoi profilarli e, se ritieni che sia necessario dopo aver esaminato i dati del profilo, allinea le tue funzioni.

L'assemblatore in genere non lo farà automaticamente.

+0

Grazie per la tua risposta! Sì, sono d'accordo - per ora rimarrò ai principi di base, ma non potrei resistere al pensiero sull'ottimizzazione. Roba affascinante! –

+0

nel complesso una risposta eccellente. Il profiling sull'assemblaggio non è sempre rilevante, perché se si deve ricorrere ad esso probabilmente si è profilato un codice C o C++ e si sono trovati elementi che devono essere affrontati e che hanno portato all'assemblaggio in primo luogo. Quello che puoi (e dovresti fare) per verificare codice che è lontano dal completamento ma il cui livello di prestazioni di base deve essere quantificato è quello di cronometrare il codice in questione usando l'istruzione rdtsc (contatore di timestamp ReaD) prima e dopo e calcolando la differenza . Questo è disponibile solo da Pentium MMX e in poi e in modalità a 32 bit. –

+1

@Scott Davies: Non è sbagliato pensare all'ottimizzazione quando si programma in assembly. Molto probabilmente lo stai facendo perché potresti desiderare qualche ottimizzazione. Ma sappi che questi suggerimenti per l'ottimizzazione forniti in questo libro erano veri circa 25 anni fa ma ora sono obsoleti o addirittura sbagliati. Davvero non vuoi seguire le tue istruzioni con i nops per farli rimanere su un indirizzo pari su un processore moderno, anche se dovesse capitare di funzionare in modalità a 16 bit. Se vuoi leggere alcune cose affascinanti, che in realtà ha qualche utilità, consiglio davvero i manuali di ottimizzazione su agner.org – hirschhornsalz

4

Poiché il processore (16 bit) può recuperare valori dalla memoria solo a indirizzi pari, a causa del suo particolare layout: è diviso in due "banchi" di 1 byte ciascuno, quindi metà del bus dati è collegato al prima banca e l'altra metà all'altra banca. Ora, supponiamo che questi banchi siano allineati (come nella mia immagine), il processore può recuperare i valori che si trovano sulla stessa "riga".

bank 1 bank 2 
+--------+--------+ 
| 8 bit | 8 bit | 
+--------+--------+ 
|  |  | 
+--------+--------+ 
| 4  | 5  | <-- the CPU can fetch only values on the same "row" 
+--------+--------+ 
| 2  | 3  | 
+--------+--------+ 
| 0  | 1  | 
+--------+--------+ 
\ /\ /
    | | | | 
    | | | | 

data bus (to uP) 

Ora, poiché questo recuperare limitazione, se la CPU è costretto a recuperare i valori che si trovano su un indirizzo dispari (supporti 3), si deve recuperare i valori a 2 e 3, i valori poi a 4 e 5 , butta via i valori 2 e 5 poi unisci 4 e 3 (stai parlando di x86, che come un layout di memoria little endian).
Ecco perché è meglio avere codice (e dati!) Su indirizzi pari.

PS: Su processori a 32 bit, codice e dati devono essere allineati su indirizzi che sono divisibili per 4 (poiché ci sono 4 banchi).

Spero di essere stato chiaro. :)

+0

", quindi valori a 4 e 5, buttare via i valori 2 e 5 quindi unire 4 e 3" puoi elaborarlo per favore? –

+0

@ user1218927 si supponga di voler caricare la parola composta dai byte 3 e 4. La CPU carica la parola all'indirizzo 2 (primo accesso alla memoria) e la parola all'indirizzo 4 (secondo accesso alla memoria); i byte memorizzati all'indirizzo 2 e 5 vengono scartati perché non sono necessari, mentre i byte memorizzati a 3 e 4 sono uniti – BlackBear

1

Il problema non è limitato ai soli recuperi di istruzioni. Ed è un peccato che i programmatori non siano stati messi a conoscenza di ciò in anticipo e puniti spesso. L'architettura x86 ha reso la gente pigra. Lo rende difficile quando si passa ad altre architetture.

Ha tutto a che fare con la natura del bus dati. Quando si dispone ad esempio di un bus dati a 32 bit, una lettura dalla memoria è allineata su quel confine. In questo caso i due bit di indirizzo inferiori vengono normalmente ignorati in quanto non hanno alcun significato. Quindi, se dovessi eseguire una lettura a 32 bit dall'indirizzo 0x02, che si tratti di una raccolta di istruzioni o di una lettura dalla memoria. Quindi sono necessari due cicli di memoria, una lettura dall'indirizzo 0x00 per ottenere due byte e una lettura da 0x04 per ottenere gli altri due byte. Prendendo il doppio del tempo, in fase di stallo della pipeline se questo è un recupero di istruzioni. Il successo della performance è drammatico e in nessun modo una ottimizzazione sprecata per la lettura dei dati. I programmi che allineano i loro dati sui confini naturali e regolano le strutture e altri oggetti in multipli interi di queste dimensioni, possono vedere il doppio delle prestazioni senza altri sforzi. Allo stesso modo usare un int invece di un char per una variabile anche se può contare fino a 10 può essere più veloce. È vero che l'aggiunta di nops ai programmi per allineare le destinazioni delle filiali non è di solito la pena. Sfortunatamente x86 ha una lunghezza di parola variabile, basata su byte e tu soffri costantemente di queste inefficienze. Se vieni dipinto in un angolo e hai bisogno di spremere un po 'più di clock da un ciclo, non dovresti solo allinearlo su un confine che corrisponde alla dimensione del bus (in questi giorni 32 o 64 bit) ma anche su un limite della linea cache, e prova a mantenere quel ciclo all'interno di una o forse due linee di cache. Su questa nota, un singolo nop casuale in un programma può causare cambiamenti in cui le linee della cache colpiscono e un cambiamento di prestazioni può essere rilevato se il programma è abbastanza grande e ha abbastanza funzioni o cicli. La stessa storia, ad esempio, si ha un obiettivo di diramazione all'indirizzo 0xFFFC, se non nella cache si deve prelevare una cacheline, niente di inaspettato, ma una o due istruzioni successive (quattro byte) è necessaria un'altra linea di cache. Se il target era stato 0x10000, a seconda della dimensione della tua funzione in modo naturale, potresti averlo rimosso in una riga della cache. Se questa è una funzione spesso chiamata e un'altra funzione spesso chiamata è in un indirizzo abbastanza simile che questi due si sfrattano a vicenda, verrà eseguito due volte più lentamente. Questo è un luogo in cui l'x86 aiuta, sebbene con una lunghezza di istruzioni variabile, è possibile impacchettare più codice in una linea di cache rispetto ad altre architetture ben utilizzate.

Con x86 e fetch di istruzioni non puoi davvero vincere. A questo punto è spesso inutile cercare di sintonizzare i programmi x86 (dal punto di vista dell'istruzione). Il numero di diversi core e le loro sfumature, è possibile ottenere guadagni su un processore su un computer un giorno, ma lo stesso codice renderà altri processori x86 su altri computer più lenti, a volte meno della metà della velocità. È meglio essere genericamente efficienti ma avere un po 'di trascuratezza per farlo funzionare bene su tutti i computer ogni giorno. L'allineamento dei dati mostrerà miglioramenti tra i processori attraverso i computer, ma l'allineamento delle istruzioni non ci sarà.

+1

La lunghezza dell'istruzione variabile non è affatto male. Un compilatore/programmatore esperto può/userà moduli di istruzione più brevi che portano a un codice più denso che, a sua volta, scaricherà la cache del codice. Per accedere al codice o ai dati da L1, L2, L3 o RAM, è possibile utilizzare un costo di circa 3, 10, 30 e 100 cicli di stallo. Qualcosa trovato in L2 istdo L1 causerà quindi 7 (10-3) cicli extra. L3 (istdo L1 e 2) 17 (30-10-3) e RAM (istdo cache) 67 (100-30-10-3). Da questo punto di vista il codice denso è abbastanza buono. –

Problemi correlati