2013-05-21 16 views
16

Mentre si passava attraverso un codice Qt, mi sono imbattuto in quanto segue. La funzione QMainWindowLayout::invalidate() ha la seguente implementazione:Perché un compilatore genera questo assieme?

void QMainWindowLayout::invalidate() 
{ 
QLayout::invalidate() 
minSize = szHint = QSize(); 
} 

Viene compilato in questo:

<invalidate()>  push %rbx 
<invalidate()+1>  mov %rdi,%rbx 
<invalidate()+4>  callq 0x7ffff4fd9090 <QLayout::invalidate()> 
<invalidate()+9>  movl $0xffffffff,0x564(%rbx) 
<invalidate()+19>  movl $0xffffffff,0x568(%rbx) 
<invalidate()+29>  mov 0x564(%rbx),%rax 
<invalidate()+36>  mov %rax,0x56c(%rbx) 
<invalidate()+43>  pop %rbx 
<invalidate()+44>  retq 

Complesso da invalidi + 9 + 36 per invalidare sembra stupido. Innanzitutto il codice scrive da -1 a% rbx + 0x564 e% rbx + 0x568, ma poi carica quello -1 da% rbx + 0x564 in un registro solo per scriverlo su% rbx + 0x56c. Questo sembra qualcosa che il compilatore dovrebbe facilmente essere in grado di ottimizzare in un'altra mossa immediata.

Quindi è questo codice stupido (e se sì, perché il compilatore non dovrebbe ottimizzarlo?) O è in qualche modo molto intelligente e veloce rispetto all'utilizzo di un'altra mossa immediata?

(Nota:.. Questo codice è dal normale accumulo biblioteca rilascio spedito da ubuntu, così è stato presumibilmente compilata da GCC in modalità ottimizzare la minSize e szHint variabili sono normali variabili di tipo QSize)

+3

QT è un'interfaccia utente, corretta? Quante volte in successione avresti bisogno di invalidare una finestra? Quanto dovrebbe essere effettivamente performante? Il tipo di micro-ottimizzazione che stai descrivendo non merita quasi certamente lo sforzo per il minimo beneficio che ne deriverebbe. –

+0

Sembra davvero non ottimale, forse l'ottimizzatore dello spioncino non l'ha capito. –

+11

@RobertHarvey Ma non è questo il punto qui: l'OP non sta cercando di ottimizzare, sta cercando di capirne il motivo. –

risposta

13
Non

sicuro hai ragione quando dici che è stupido. Penso che il compilatore potrebbe tentare di ottimizzare la dimensione del codice qui. Non ci sono istruzioni mov per memoria immediata a 64 bit. Quindi il compilatore deve generare 2 istruzioni mov proprio come sopra. Ognuno di loro sarebbe 10 byte, le 2 mosse generate sono 14 byte. È stato scritto così non c'è probabilmente alcuna latenza di memoria, quindi non penso che tu possa prendere alcun risultato in termini di prestazioni qui.

+0

... e inoltre, se si fa un 'mov ..., (addr)' seguito da un 'mov (addr), ...' allora il secondo è cache-hot, cioè c'è poca penalità per questo. L'unica ottimizzazione che riesco a pensare qui sarebbe stata 'pcmpeq% xmm0,% xmm0; movdqu% xmm0, 0x564 (% rbx) 'per impostare l'intero 16 byte a tutti' 0xff..', ma è piuttosto difficile chiedere di "unire" due variabili in questo modo - e probabilmente non del tutto conforme agli standard wrt. a C++ carico/deposito garanzie di visibilità. –

+2

+1 per * "Non vi è alcuna istruzione mov in memoria immediata a 64 bit," * è tutto ciò che occorre dire. –

+0

Non conoscevo la parte relativa alla mossa senza 64 bit immediata, quindi questa è probabilmente la soluzione. Inoltre, non sembra esserci alcun costo reale su x86 se un accesso di memoria non allineato non attraversa i limiti della linea cache – JanKanis

1

avrei abbattere le linee come questo (si pensi molti hanno commento stessi passi)

Queste due linee deriva dalla definizione in linea di QSize()http://qt.gitorious.org/qt/qt/blobs/4.7/src/corelib/tools/qsize.h che ha fissato ogni campo separatamente. Inoltre, suppongo che 0x564 (% rbx) sia l'indirizzo di szHint, anch'esso impostato allo stesso tempo.

<invalidate()+9>  movl $0xffffffff,0x564(%rbx) 
<invalidate()+19>  movl $0xffffffff,0x568(%rbx) 

Queste linee sono infine fissando minSize tramite operazioni a 64 bit perché il compilatore ora conosce la dimensione di un oggetto QSize. E l'indirizzo del minSize è 0x56c (% rbx)

<invalidate()+29>  mov 0x564(%rbx),%rax 
<invalidate()+36>  mov %rax,0x56c(%rbx) 

Nota. La prima parte sta impostando due campi separati e la parte successiva sta copiando un oggetto QSize (indipendentemente dal contenuto). La domanda quindi è, dovrebbe il compilatore essere abbastanza intelligente per costruire un valore composto a 64 bit perché è ha visto valori predefiniti appena precedenti? Non sono sicuro ...

+2

Sì, i compilatori sono generalmente in grado di fare questo tipo di ottimizzazioni. È conosciuto come piegamento costante. – JanKanis

+0

@Somejan Cool, non lo sapevo :) – epatel

0

In aggiunta alla risposta di Guillaume, il carico/archivio a 64 bit non è allineato. Ma secondo lo Intel optimization guide (p 3-62)

L'accesso ai dati disallineato può comportare sanzioni significative in termini di prestazioni. Ciò è particolarmente vero per le divisioni della linea cache. La dimensione di una linea cache è 64 byte nel Pentium 4 e altri processori Intel recenti, inclusi processori basati su microarchitettura Intel Core.

Un accesso ai dati non allineati sul limite di 64 byte porta a due accessi alla memoria e richiede l'esecuzione di più μops (anziché uno). Gli accessi che si estendono su limiti di 64 byte sono soggetti a una grande penalità di prestazioni , il costo di ogni stallo in genere è maggiore sulle macchine con pipeline più lunghe.

Quale imo implica che un carico/archivio non allineato che non attraversa un limite della linea cache è economico. In questo caso il puntatore di base nel processo che stavo eseguendo il debug era 0x10f9bb0, quindi le due variabili sono 20 e 28 byte nella cacheline.

Normalmente i processori Intel utilizzano l'archivio per caricare l'inoltro, quindi un carico di un valore appena memorizzato non ha nemmeno bisogno di toccare la cache. Ma la stessa guida afferma anche che un grande carico di diversi negozi più piccoli non immagazzina-carica-avanti ma bancarelle: (p 3-66, p 3-68)

Regole di assemblaggio/compilatore Cod. 49. (H impatto, M generalità) I dati di un carico che viene inoltrato da un negozio devono essere completamente contenuti all'interno dei dati del negozio.

; A. Large load stall 
mov  mem, eax  ; Store dword to address “MEM" 
mov  mem + 4, ebx ; Store dword to address “MEM + 4" 
fld  mem    ; Load qword at address “MEM", stalls 

Così il codice in questione probabilmente provoca una stalla, e quindi sono propenso a credere che non è ottimale. Non sarei molto sorpreso se GCC non tenga pienamente conto di tali limitazioni. Qualcuno sa se/quanto modella i limiti di inoltro da magazzino a carico GCC?

MODIFICA: alcuni esperimenti con l'aggiunta di valori di riempimento prima che i campi minSize/szHint mostrano che a GCC non interessa affatto dove sono i limiti della linea di cache, e nessuno dei due fa clang.

8

Il codice è "meno che perfetto".

Per le dimensioni del codice, queste 4 istruzioni sommano fino a 34 byte. Una sequenza molto più piccola (19 byte) è possibile:

00000000 31C0    xor eax,eax 
00000002 48F7D0   not rax 
00000005 48898364050000 mov [rbx+0x564],rax 
0000000C 4889836C050000 mov [rbx+0x56c],rax 

;Note: XOR above clears RAX due to zero extension 

Per le prestazioni le cose non sono così semplici. La CPU vuole fare molte istruzioni allo stesso tempo e il codice sopra lo spezza. Per esempio:

xor eax,eax 
not rax     ;Must wait until previous instruction finishes 
mov [rbx+0x564],rax  ;Must wait until previous instruction finishes 
mov [rbx+0x56c],rax  ;Must wait until "not" finishes 

Per le prestazioni che si vuole fare questo:

00000000 48C7C0FFFFFFFF  mov rax,0xffffffff 
00000007 C78364050000FFFFFFFF mov dword [rbx+0x564],0xffffffff 
00000011 C78368050000FFFFFFFF mov dword [rbx+0x568],0xffffffff 
0000001B C7836C050000FFFFFFFF mov dword [rbx+0x56c],0xffffffff 
00000025 C78370050000FFFFFFFF mov dword [rbx+0x570],0xffffffff 

;Note: first MOV sets RAX to 0xFFFFFFFFFFFFFFFF due to sign extension 

Questo permette tutte le istruzioni da eseguire in parallelo, senza dipendenze ovunque. Purtroppo, è anche molto più grande (45 byte).

Se si tenta di ottenere un equilibrio tra dimensioni del codice e prestazioni; allora potresti sperare che la prima istruzione (che imposta il valore in RAX) completi prima che l'ultima/le ultime istruzioni abbia bisogno di conoscere il valore in RAX. Questo potrebbe essere qualcosa del genere:

mov rax,-1 
mov dword [rbx+0x564],0xffffffff 
mov dword [rbx+0x568],0xffffffff 
mov dword [rbx+0x56c],rax 

Questo è 34 byte (la stessa dimensione del codice originale). Questo è probabilmente un buon compromesso tra dimensioni del codice e prestazioni.

Ora; diamo un'occhiata al codice originale e vedere perché è male:

mov dword [rbx+0x564],0xffffffff 
mov dword [rbx+0x568],0xffffffff 
mov rax,[rbx+0x564]    ;Massive problem 
mov [rbx+0x56C],rax    ;Depends on previous instruction 

moderne CPU hanno una cosa chiamata "store forwarding", dove scrive vengono memorizzati in un buffer e futura legge può ottenere il valore da questo buffer per evitare leggendo il valore dalla cache.Ironia della sorte, questo funziona solo se la dimensione della lettura è inferiore o uguale alla dimensione della scrittura. Il "forwarding del negozio" non funzionerà per questo codice poiché ci sono 2 scritture e la lettura è più grande di entrambe. Ciò significa che la terza istruzione deve attendere fino a quando le prime 2 istruzioni hanno scritto nella cache e quindi deve leggere il valore dalla cache; che potrebbe facilmente aggiungere fino a una penalità di circa 30 cicli o più. Quindi la quarta istruzione deve attendere la terza istruzione (e non può accadere in parallelo con qualcosa), quindi questo è un altro problema.

+0

+1 per l'utilizzo della sintassi Intel. Domanda veloce, il codice originale ha 'mov [rbx + 0x56C], rax' ma nel tuo esempio ottimizzato' mov dword [rbx + 0x56C], rax'. Significa questo lo spostamento originale di 8 byte (QWORD) in '[rbx + 0x56c]' mentre il tuo spostamento di 4 byte (DWORD)? È questo? – greatwolf

Problemi correlati