2011-02-04 15 views
5

Ho un segmento di codice che è il più semplice:Intel C++ Compiler comprensione di ciò che l'ottimizzazione viene eseguita

for(int i = 0; i < n; ++i) 
{ 
    if(data[i] > c && data[i] < r) 
    { 
    --data[i]; 
    } 
} 

E 'una parte di una grande funzionalità e di progetto. Questo è in realtà una riscrittura di un ciclo diverso, che ha dimostrato di essere in termini di tempo (lunghi loop), ma sono rimasto sorpreso da due cose:

Quando i dati [i] era temporaneo salvato in questo modo:

for(int i = 0; i < n; ++i) 
{ 
    const int tmp = data[i]; 
    if(tmp > c && tmp < r) 
    { 
    --data[i]; 
    } 
} 

È diventato molto più lento. Non pretendo che questo dovrebbe essere più veloce, ma non riesco a capire perché dovrebbe essere molto più lento, il compilatore dovrebbe essere in grado di capire se il tmp dovrebbe essere usato o meno.

Ma ancora più importante quando ho spostato il segmento di codice in una funzione separata è diventato circa quattro volte più lento. Volevo capire cosa stava succedendo, quindi ho esaminato il referto opt-report e in entrambi i casi il ciclo è stato vettorializzato e sembra che faccia la stessa ottimizzazione.

Quindi la mia domanda è che cosa può fare una tale differenza su una funzione che non è chiamata un milione di volte, ma richiede molto tempo? Cosa cercare nel rapporto opt-?

Potrei evitarlo semplicemente mantenendolo in linea, ma il motivo è che mi infastidisce.

UPDATE:

dovrei sottolineare che la mia preoccupazione principale è quello di capire, perché è diventato più lento, quando viene spostato ad una funzione separata. L'esempio di codice fornito con la variabile tmp, era solo uno strano esempio che ho incontrato durante il processo.

+1

Proprio la domanda obbligatoria: hai ottimizzazioni max su tutte le build? – GManNickG

+2

Prova a dare un'occhiata all'assieme generato. –

+0

Sì, tutto è compilato con O3. Non ho ancora esaminato l'assemblea, semplicemente perché, come detto, è parte di un grande progetto (anche perché non sono un esperto di assemblaggio). –

risposta

4

Probabilmente sei registrato affamato e il compilatore deve caricare e archiviare. Sono abbastanza sicuro che le istruzioni di assemblaggio native x86 possano prendere gli indirizzi di memoria per operare on-i.e., il compilatore può mantenere liberi quei registri. Ma rendendolo locale, è possibile modificare il comportamento wrt. l'aliasing e il compilatore potrebbero non essere in grado di dimostrare che la versione più veloce ha la stessa semantica, specialmente se qui c'è qualche forma di thread multipli, permettendogli di cambiare il codice.

La funzione era più lenta in un nuovo segmento, probabilmente perché le chiamate di funzione non solo possono interrompere la pipeline, ma anche creare scarse prestazioni della cache di istruzioni (c'è un codice aggiuntivo per il parametro push/pop/etc).

Lezione: Lascia che il compilatore esegua l'ottimizzazione, è più intelligente di te. Non intendo questo come un insulto, è più intelligente di me. Ma davvero, specialmente il compilatore Intel, quei ragazzi sanno quello che stanno facendo quando si tratta della loro piattaforma.

Modifica: Ancora più importante, è necessario riconoscere che i compilatori sono mirati all'ottimizzazione del codice non ottimizzato.Non sono mirati a riconoscere il codice semi-ottimizzato. Nello specifico, il compilatore avrà una serie di trigger per ogni ottimizzazione e, se ti capita di scrivere il tuo codice in modo tale da non essere colpito, puoi evitare di eseguire le ottimizzazioni anche se il codice è semanticamente identico allo.

E inoltre è necessario considerare i costi di implementazione. Non tutte le funzioni ideali per l'inlining possono essere sottolineate, solo perché la logica è troppo complessa per essere gestita dal compilatore. So che VC++ sarà raramente in linea con i loop, anche se i rendimenti inlining ne trarranno beneficio. Potresti vedere questo nel compilatore Intel - che gli scrittori di compilatori hanno semplicemente deciso che non valeva la pena impiegare il tempo.

L'ho riscontrato quando si trattava di loop in VC++ - il compilatore avrebbe prodotto diversi assembly per due loop in formati leggermente diversi, anche se entrambi hanno ottenuto lo stesso risultato. Ovviamente, la loro libreria Standard ha utilizzato il formato ideale. È possibile osservare un aumento di velocità utilizzando std::for_each e un oggetto funzione.

+0

Concordo sulla lezione, ma non volevo essere più intelligente del compilatore, solo per capire e imparare a migliorare il codice per adattarlo meglio al compilatore. Il vero problema è capire perché è diventato più lento quando spostato su un altro file. –

+0

@Bo Jensen: Questo perché molti compilatori non possono eseguire l'inlining tra le unità di traduzione a causa del modo in cui è progettato C++. Credo che le versioni recenti di VC++ possano fare questo e forse anche GCC, ma non credo che Intel possa farlo. – Puppy

+0

grazie per la tua risposta. Ma la funzione è nello stesso file. –

1

Hai ragione, il compilatore dovrebbe essere in grado di identificarlo come codice inutilizzato e rimuoverlo/non compilarlo. Ciò non significa che lo identifichi e rimuoverlo.

La soluzione migliore è guardare l'assemblaggio generato e controllare per vedere esattamente cosa sta succedendo. Ricorda, solo perché un compilatore intelligente potrebbe essere in grado di capire come fare un'ottimizzazione, non significa che possa farlo.

Se si verifica, e si vede che il codice non viene rimosso, è possibile segnalarlo al team del compilatore Intel. Sembra che potrebbero avere un bug.

-2

Sono stupito che questo

for(int i = 0; i < n; ++i) 
{ 
    const int tmp = data[i]; //?? declaration inside a loop 
    if(tmp > c && tmp < r) 
    { 
    --data[i]; 
    } 
} 

compila a tutti. Probabilmente confonde il compilatore. Prova

for(int tmp, i = 0; i < n; ++i) 
{ 
    tmp = data[i]; 
    if(tmp > c && tmp < r) 
    { 
    --data[i]; 
    } 
} 

invece. Generalmente usa size_t (uint) per eseguire il loopover. Gli int registrati sono diversamente codificati da quelli non firmati, quindi potrebbe esserci un bithift non necessario. Quindi proverei

int tmp; // well if you must have your temporary, I don't see why you want it, 
     // it costs you 1 register although that should not matter much here. 
for(size_t i = 0; i < n; ++i) 
{ 
    tmp = data[i]; 
    if(tmp > c && tmp < r) 
    { 
    --data[i]; 
    } 
} 

Pubblica i tuoi risultati.

+0

Perché non dovrebbe essere compilato? È un codice perfettamente legale. Anche cosa bitshift? L'incremento o il decremento non richiede alcun cambio di bit. –

+0

È legale ma è insolito. Gli indici sono int non firmati quindi è necessaria una conversione da 2-complemento che è la codifica di int. – supertux

Problemi correlati