Lavoro sul progetto STAPL che è una libreria C ++ fortemente basata su modelli. Di tanto in tanto, dobbiamo rivisitare tutte le tecniche per ridurre i tempi di compilazione. Qui ho riassunto le tecniche che usiamo. Alcune di queste tecniche sono già elencate sopra:
Trovare le sezioni che richiedono più tempo
Sebbene non sia stata dimostrata una correlazione tra la lunghezza dei simboli e il tempo di compilazione, abbiamo osservato che dimensioni medie dei simboli più piccole possono migliorare il tempo di compilazione su tutti i compilatori. Quindi i tuoi primi obiettivi è quello di trovare i simboli più grandi nel tuo codice.
Metodo 1: ordina i simboli in base alle dimensioni
Puoi usare il nm
comando per elencare i simboli in base alle loro dimensioni:
nm --print-size --size-sort --radix=d YOUR_BINARY
In questo comando il --radix=d
ti consente di vedere le dimensioni in numeri decimali (il valore predefinito è esadecimale). Ora osservando il simbolo più grande, identifica se puoi interrompere la classe corrispondente e prova a ridisegnarla fattorizzando le parti non modellate in una classe base o suddividendo la classe in più classi.
Metodo 2: ordina i simboli in base alla lunghezza
Puoi eseguire il nm
comando normale e reindirizzarlo al tuo script preferito ( AWK , Python , ecc.) Per ordinare i simboli in base alla loro lunghezza . Sulla base della nostra esperienza, questo metodo identifica il problema maggiore nel rendere i candidati migliori del metodo 1.
Metodo 3: utilizzare Templight
" Templight è un Clang strumento basato su il tempo e il consumo di memoria delle istanze dei modelli e per eseguire sessioni di debug interattive per ottenere introspezione nel processo di istanza dei modelli".
È possibile installare Templight controllando LLVM e Clang ( istruzioni ) e applicando la patch Templight su di esso. L'impostazione predefinita per LLVM e Clang è su debug e asserzioni e questi possono influire in modo significativo sui tempi di compilazione. Sembra che Templight abbia bisogno di entrambi, quindi devi usare le impostazioni predefinite. Il processo di installazione di LLVM e Clang dovrebbe durare circa un'ora.
Dopo aver applicato la patch è possibile utilizzare templight++
situato nella cartella build specificata al momento dell'installazione per compilare il codice.
Assicurati che templight++
sia nel tuo PERCORSO. Ora per compilare aggiungi le seguenti opzioni al tuo CXXFLAGS
nel tuo Makefile o alle opzioni della tua riga di comando:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
O
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Al termine della compilazione, avrai un .trace.memory.pbf e .trace.pbf generati nella stessa cartella. Per visualizzare queste tracce, è possibile utilizzare gli strumenti Templight che possono convertirli in altri formati. Segui queste istruzioni per installare templight-convert. Di solito usiamo l'output di callgrind. Puoi anche usare l'output di GraphViz se il tuo progetto è piccolo:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
Il file callgrind generato può essere aperto usando kcachegrind in cui è possibile tracciare la maggiore istanza che richiede tempo / memoria.
Riduzione del numero di istanze del modello
Sebbene non esista una soluzione per ridurre il numero di istanze di modelli, esistono alcune linee guida che possono aiutare:
Classi refactor con più di un argomento template
Ad esempio, se hai una lezione,
template <typename T, typename U>
struct foo { };
ed entrambi T
e U
con 10 diverse opzioni, hai aumentato le possibili istanze di modello di questa classe a 100. Un modo per risolverlo è quello di astrarre la parte comune del codice in una classe diversa. L'altro metodo consiste nell'usare l'inversione dell'ereditarietà (invertendo la gerarchia di classi), ma assicurarsi che i propri obiettivi di progettazione non vengano compromessi prima di utilizzare questa tecnica.
Ricalcola il codice non basato su modelli in singole unità di traduzione
Usando questa tecnica, puoi compilare una volta la sezione comune e collegarla successivamente con le altre TU (unità di traduzione).
Usa le istanze di template esterne (dal C ++ 11)
Se conosci tutte le possibili istanze di una classe puoi usare questa tecnica per compilare tutti i casi in una diversa unità di traduzione.
Ad esempio, in:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Sappiamo che questa classe può avere tre possibili istanze:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Inserisci quanto sopra in un'unità di traduzione e utilizza la parola chiave extern nel file di intestazione, sotto la definizione della classe:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Questa tecnica può farti risparmiare tempo se stai compilando diversi test con un insieme comune di istanze.
NOTA: MPICH2 ignora l'istanza esplicita a questo punto e compila sempre le classi istanziate in tutte le unità di compilazione.
Usa build di unità
L'idea alla base di unità build è quella di includere tutti i file .cc che usi in un file e compilarlo solo una volta. Usando questo metodo, puoi evitare di reintegrare sezioni comuni di file diversi e se il tuo progetto include molti file comuni, probabilmente risparmierai anche sugli accessi al disco.
Per fare un esempio, supponiamo di avere tre file foo1.cc
, foo2.cc
, foo3.cc
e sono tutte dotate tuple
di STL . Puoi creare un foo-all.cc
aspetto simile a:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Compilare questo file solo una volta e potenzialmente ridurre le istanze comuni tra i tre file. È difficile prevedere in generale se il miglioramento può essere significativo o meno. Ma un fatto evidente è che perderai il parallelismo nelle tue build (non puoi più compilare i tre file contemporaneamente).
Inoltre, se uno di questi file richiede molta memoria, potresti effettivamente esaurire la memoria prima che la compilation sia terminata. Su alcuni compilatori, come GCC , questo potrebbe ICE (Internal Compiler Error) il compilatore per mancanza di memoria. Quindi non usare questa tecnica se non conosci tutti i pro e i contro.
Intestazioni precompilate
Le intestazioni precompilate (PCH) possono farti risparmiare molto tempo nella compilazione compilando i file di intestazione in una rappresentazione intermedia riconoscibile da un compilatore. Per generare file di intestazione precompilati, è sufficiente compilare il file di intestazione con il normale comando di compilazione. Ad esempio, su GCC:
$ g++ YOUR_HEADER.hpp
Questo genererà un YOUR_HEADER.hpp.gch file
( .gch
è l'estensione per i file PCH in GCC) nella stessa cartella. Ciò significa che se includi YOUR_HEADER.hpp
in qualche altro file, il compilatore utilizzerà prima il tuo YOUR_HEADER.hpp.gch
invece che YOUR_HEADER.hpp
nella stessa cartella.
Esistono due problemi con questa tecnica:
- Devi assicurarti che i file di intestazione precompilati siano stabili e non cambieranno ( puoi sempre cambiare il tuo makefile )
- Puoi includere solo un PCH per unità di compilazione (sulla maggior parte dei compilatori). Ciò significa che se si devono precompilare più file di intestazione, è necessario includerli in un file (ad es
all-my-headers.hpp
.). Ciò significa che devi includere il nuovo file in tutti i luoghi. Fortunatamente, GCC ha una soluzione per questo problema. Usa -include
e dagli il nuovo file di intestazione. È possibile separare i diversi file con virgola utilizzando questa tecnica.
Per esempio:
g++ foo.cc -include all-my-headers.hpp
Usa spazi dei nomi anonimi o anonimi
Gli spazi dei nomi senza nome (noti anche come spazi dei nomi anonimi) possono ridurre significativamente le dimensioni binarie generate. Gli spazi dei nomi senza nome utilizzano un collegamento interno, il che significa che i simboli generati in tali spazi dei nomi non saranno visibili ad altre unità di traduzione (unità di traduzione o compilazione). I compilatori di solito generano nomi univoci per spazi dei nomi senza nome. Ciò significa che se si dispone di un file foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
E ti capita di includere questo file in due TU (due file .cc e compilarli separatamente). Le due istanze di foo template non saranno le stesse. Ciò viola la One Definition Rule (ODR). Per lo stesso motivo, l'utilizzo di spazi dei nomi senza nome è sconsigliato nei file di intestazione. Sentiti libero di usarli nei tuoi .cc
file per evitare che i simboli vengano visualizzati nei tuoi file binari. In alcuni casi, la modifica di tutti i dettagli interni per un .cc
file ha mostrato una riduzione del 10% nelle dimensioni binarie generate.
Modifica delle opzioni di visibilità
Nei compilatori più recenti è possibile selezionare i simboli in modo che siano visibili o invisibili nei Dynamic Shared Objects (DSO). Idealmente, la modifica della visibilità può migliorare le prestazioni del compilatore, le ottimizzazioni del tempo di collegamento (LTO) e le dimensioni binarie generate. Se guardi i file di intestazione STL in GCC puoi vedere che è ampiamente usato. Per abilitare le opzioni di visibilità, è necessario modificare il codice per funzione, per classe, per variabile e, soprattutto, per compilatore.
Con l'aiuto della visibilità puoi nascondere i simboli che li consideri privati dagli oggetti condivisi generati. Su GCC puoi controllare la visibilità dei simboli passando di default o nascosto -visibility
all'opzione del tuo compilatore. Questo è in qualche modo simile allo spazio dei nomi senza nome, ma in un modo più elaborato e invadente.
Se desideri specificare le visibilità per caso, devi aggiungere i seguenti attributi alle tue funzioni, variabili e classi:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
La visibilità predefinita in GCC è predefinita (pubblica), il che significa che se si compila quanto sopra come -shared
metodo di libreria condivisa ( ) foo2
e la classe foo3
non sarà visibile in altre TU ( foo1
e foo4
sarà visibile). Se lo compili con -visibility=hidden
solo allora foo1
sarà visibile. Anche foo4
sarebbe nascosto.
Puoi leggere di più sulla visibilità sul wiki di GCC .