Perché l'ottimizzazione del linker è così scarsa?
-
05-07-2019 - |
Domanda
Di recente, un collega mi ha fatto notare che la compilazione di tutto in un singolo file ha creato un codice molto più efficiente rispetto alla compilazione di file di oggetti separati - anche con l'ottimizzazione del tempo di collegamento attivata . Inoltre, il tempo totale di compilazione per il progetto è diminuito in modo significativo. Dato che uno dei motivi principali per l'utilizzo del C ++ è l'efficienza del codice, questo mi ha sorpreso.
Chiaramente, quando l'archiviatore / linker crea una libreria dai file oggetto o li collega in un eseguibile, anche le semplici ottimizzazioni vengono penalizzate. Nell'esempio seguente, il banale allineamento costa l'1,8% in termini di prestazioni quando viene eseguito dal linker anziché dal compilatore. Sembra che la tecnologia del compilatore dovrebbe essere sufficientemente avanzata per gestire situazioni abbastanza comuni come questa, ma non sta accadendo.
Ecco un semplice esempio usando Visual Studio 2008:
#include <cstdlib>
#include <iostream>
#include <boost/timer.hpp>
using namespace std;
int foo(int x);
int foo2(int x) { return x++; }
int main(int argc, char** argv)
{
boost::timer t;
t.restart();
for (int i=0; i<atoi(argv[1]); i++)
foo (i);
cout << "time : " << t.elapsed() << endl;
t.restart();
for (int i=0; i<atoi(argv[1]); i++)
foo2 (i);
cout << "time : " << t.elapsed() << endl;
}
foo.cpp
int foo (int x) { return x++; }
Risultati della corsa: prestazioni dell'1,8% colpite dall'uso di foo
anziché inline foo2
.
$ ./release/testlink.exe 100000000
time : 13.375
time : 13.14
E sì, i flag di ottimizzazione del linker (/ LTCG) sono attivi.
Soluzione
Non sono uno specialista del compilatore, ma penso che il compilatore abbia molte più informazioni disponibili per ottimizzare in quanto opera su un albero linguistico, al contrario del linker che deve accontentarsi per operare sull'output dell'oggetto, molto meno espressivo del codice visto dal compilatore. Quindi meno sforzi vengono spesi dai team di sviluppo del linker e del compilatore per realizzare ottimizzazioni del linker che potrebbero corrispondere, in teoria, ai trucchi del compilatore.
A proposito, mi dispiace di aver distratto la tua domanda originale nella discussione ltcg. Ora capisco che la tua domanda era un po 'diversa, più preoccupata del tempo di collegamento rispetto alle ottimizzazioni statiche del tempo di compilazione possibili / disponibili.
Altri suggerimenti
Il tuo collega non è aggiornato. La tecnologia è qui dal 2003 (sul compilatore MS C ++): / LTCG . La generazione del time code di collegamento sta affrontando esattamente questo problema. Da quello che so GCC ha questa funzionalità sul radar per il compilatore di prossima generazione.
LTCG non solo ottimizza il codice come funzioni di allineamento tra i moduli, ma in realtà riorganizza il codice per ottimizzare la localizzazione della cache e la ramificazione per un carico specifico, vedere Ottimizzazioni guidate dal profilo . Queste opzioni sono normalmente riservate solo ai build di Release poiché il build può richiedere ore per terminare: collegherà un eseguibile strumentato, eseguirà un caricamento di profiling e quindi collegherà nuovamente con i risultati di profiling. Il link contiene dettagli su ciò che è esattamente ottimizzato con LTCG:
Inline & # 8211; Ad esempio, se presente esiste una funzione A che frequentemente chiama la funzione B e la funzione B è relativamente piccolo, quindi guidato dal profilo le ottimizzazioni incorporeranno la funzione B nella funzione A.
Speculazione di chiamata virtuale & # 8211; Se una chiamata virtuale o altra chiamata tramite a puntatore a funzione, spesso target a una certa funzione, un profilo guidato l'ottimizzazione può inserire a chiamata diretta eseguita in modo condizionale a la funzione di destinazione frequente e la chiamata diretta può essere incorporata.
Allocazione dei registri & # 8211; Ottimizzare con i dati del profilo risultano migliori allocazione del registro.
Ottimizzazione dei blocchi di base & # 8211; Blocco di base l'ottimizzazione consente di eseguire comunemente blocchi di base che vengono eseguiti temporaneamente all'interno di una determinata cornice da inserire lo stesso insieme di pagine (località). Questo minimizza il numero di pagine utilizzate, minimizzando così l'overhead di memoria.
Ottimizzazione dimensioni / velocità & # 8211; funzioni dove il programma trascorre molto tempo può essere ottimizzato per la velocità.
Layout delle funzioni & # 8211; Sulla base della chiamata grafico e profilo / chiamante chiamato comportamento, funzioni che tendono ad essere lungo lo stesso percorso di esecuzione sono inserito nella stessa sezione.
Ottimizzazione del ramo condizionale & # 8211; Con le sonde di valore, guidate dal profilo le ottimizzazioni possono trovare se un dato viene utilizzato il valore in un'istruzione switch più spesso di altri valori. Questo il valore può quindi essere estratto da istruzione switch. Lo stesso si può fare con istruzioni if ??/ else dove l'ottimizzatore può ordinare if / else così che sia il blocco if oppure else posizionato per primo a seconda del blocco è più spesso vero.
Separazione del codice morto & # 8211; Codice che è non chiamato durante la profilazione viene spostato a una sezione speciale che viene aggiunta fino alla fine del set di sezioni. Questo mantiene efficacemente questa sezione dalle pagine usate di frequente.
Separazione codice EH & # 8211; Il codice EH, essendo eccezionalmente eseguito, can spesso viene spostato in una sezione separata quando le ottimizzazioni guidate dal profilo possono determinare che si verificano eccezioni solo a condizioni eccezionali.
Memory Intrinsics & # 8211; L'espansione di i intrinseci possono essere decisi meglio se può essere determinato se un intrinseco è chiamato frequentemente. Una lattina intrinseca anche essere ottimizzato in base al blocco dimensione delle mosse o delle copie.
Il tuo collega è più intelligente della maggior parte di noi. Anche se all'inizio sembra un approccio approssimativo, il progetto in linea in un singolo file .cpp ha una cosa che gli altri approcci come l'ottimizzazione del tempo di collegamento non ha e non avrà per un po '- affidabilità
Tuttavia, me lo hai chiesto due anni fa e testimonio che da allora sono cambiate molte cose (almeno con g ++). La devirtualizzazione è molto più affidabile, ad esempio.