2015-07-21 15 views
5

Una funzione banale sto compilazione con gcc e clang:Perché llvm e gcc usano prologi di funzioni differenti su x86 64?

void test() { 
    printf("hm"); 
    printf("hum"); 
} 


$ gcc test.c -fomit-frame-pointer -masm=intel -O3 -S 

sub rsp, 8 
.cfi_def_cfa_offset 16 
mov esi, OFFSET FLAT:.LC0 
mov edi, 1 
xor eax, eax 
call __printf_chk 
mov esi, OFFSET FLAT:.LC1 
mov edi, 1 
xor eax, eax 
add rsp, 8 
.cfi_def_cfa_offset 8 
jmp __printf_chk 

E

$ clang test.c -mllvm --x86-asm-syntax=intel -fomit-frame-pointer -O3 -S  

# BB#0: 
push rax 
.Ltmp1: 
.cfi_def_cfa_offset 16 
mov edi, .L.str 
xor eax, eax 
call printf 
mov edi, .L.str1 
xor eax, eax 
pop rdx 
jmp printf     # TAILCALL 

La differenza che mi interessa è che gcc usa sub rsp, 8/add rsp, 8 per la funzione prolog e clang utilizza push rax/pop rdx.

Perché i compilatori utilizzano diversi prologhi di funzione? Quale variante è migliore? push e pop codificano sicuramente per istruzioni più brevi ma sono più veloci o più lenti di add e sub?

Il motivo per cui la pila di puntini sembra essere il fatto che l'abi richiede che rsp sia 16 byte allineati per le procedure non foglia. Non sono stato in grado di trovare alcun flag del compilatore che li rimuove.

A giudicare dalle vostre risposte, sembra che il push del pop & sia migliore. push rax + pop rdx = 1 + 1 = 2 vs. sub rsp, 8 + add rsp, 8 = 4 + 4 = 8. Quindi la prima coppia risparmia 6 byte senza spese.

+0

È una questione di scelta. È difficile dire quale variante è migliore. Probabilmente entrambe le varianti sono piuttosto simili in termini di prestazioni. –

+0

re: la tua modifica. Sì, l'ABI garantisce che all'entrata della funzione, '(% rsp + 8)' è allineata a 16B. (Ho editato la maggior parte di questo commento nella mia risposta). –

risposta

8

su Intel, sub/add attiverà il motore di stack per inserire un extra uop per sincronizzare %rsp per la parte di esecuzione fuori servizio della pipeline. (Vedere Agner Fog's microarch doc, specificamente pg 91, sul motore di stack. AFAIK, funziona ancora lo stesso su Haswell come il Pentium M, per quanto quando è necessario inserire UOP extra.

Il push/pop avrà meno fusa -domain uops, e quindi probabilmente sono più efficienti anche se usano le porte store/load.Vengono tra le coppie call/ret.

Quindi, push/pop almeno non sarà più lento, ma richiede meno istruzioni byte Migliore densità della I-cache è buona

BTW, penso che il punto della coppia di insns sia quello di mantenere lo stack allineato 16B af ter call inserisce l'indirizzo di ritorno 8B. Questo è un caso in cui l'ABI finisce per richiedere istruzioni semi-inutili. Funzioni più complesse che richiedono dello spazio di stack per rovesciare i locali e quindi ricaricarle dopo le chiamate di funzione, in genere sono sub $something, %rsp per riservare spazio.

SystemV (Linux) amd64 ABI garantisce che all'ingresso della funzione, (%rsp + 8), dove saranno presenti gli argomenti nello stack, se ce ne sono, sarà allineato a 16B. (http://x86-64.org/documentation/abi.pdf). Devi fare in modo che ciò avvenga per qualsiasi funzione che chiami, o è colpa tua se si segfault dall'uso di un carico allineato SSE. In alternativa, arresta il sistema per fare ipotesi su come utilizzare AND per mascherare un indirizzo o qualcosa del genere.

+0

Sì, questo è solo per mantenere allineato lo stack. – WhatsUp

+1

Si noti inoltre che la maggior parte delle funzioni temporali alloca spazio per le variabili locali e la variante 'sub' è più efficiente in quel caso. Presumibilmente gli autori di compilatori non si sono ottimizzati per il caso in cui non sono necessari locali. – Jester

+0

Sì, le funzioni non a foglia con pochissimi locali sono un caso raro. Penso che l'uso di 'push' /' pop' di clang di dati a cui non interessa sia un'ottimizzazione accurata. –

1

In base agli esperimenti effettuati sulla mia macchina, push/pop hanno la stessa velocità di add/sub. Immagino che dovrebbe essere il caso per tutti i computer mordern.

In ogni caso, la differenza (se presente) è davvero micro-scopica, quindi vi consiglio di tranquillamente supporre che essi sono equivalenti ...

+0

Che tipo di esperimento? Stavi testando qualcosa che è stato strozzato sul throughput di uop? Sono d'accordo che probabilmente non c'è differenza per la maggior parte del tempo. –

+0

Ho fatto la cosa più ingenua: copia-incolla un'istruzione diverse migliaia di volte (in realtà usando le macro), metti il ​​tutto in un ciclo e corri. Non sono sicuro se questo è strozzato su uop. Potresti confermare? – WhatsUp

+0

'add' con gli stessi registri ogni volta che ha bisogno dell'output del precedente come input, rendendo la latenza il limitatore. 'add' ha un throughput di 3 per ciclo su SnB/IvB e 4 per ciclo su Haswell, se sono indipendenti. 'push' può sostenere 1/ciclo,' pop' 2/ciclo. Come sempre con le moderne CPU, ciò che conta è il contesto (quali altri insns sono in competizione per le risorse di esecuzione e come si inserisce in una catena di dipendenze). –

Problemi correlati