L'istanziazione esplicita consente di ridurre i tempi di compilazione e le dimensioni degli oggetti
Questi sono i principali vantaggi che può fornire. Derivano dai seguenti due effetti descritti in dettaglio nelle sezioni seguenti:
- rimuovere le definizioni dalle intestazioni per impedire agli strumenti di compilazione di ricostruire includer
- ridefinizione degli oggetti
Rimuovi le definizioni dalle intestazioni
L'istanziazione esplicita consente di lasciare le definizioni nel file .cpp.
Quando la definizione è sull'intestazione e la modifichi, un sistema di compilazione intelligente ricompila tutti gli includer, che potrebbero essere dozzine di file, rendendo la compilazione insopportabilmente lenta.
L'inserimento delle definizioni nei file .cpp ha lo svantaggio che le librerie esterne non possono riutilizzare il modello con le loro nuove classi, ma "Rimuovi le definizioni dalle intestazioni incluse ma esponi anche i modelli un'API esterna" di seguito mostra una soluzione alternativa.
Vedi esempi concreti di seguito.
La ridefinizione degli oggetti guadagna: comprensione del problema
Se si definisce completamente un modello su un file di intestazione, ogni singola unità di compilazione che include quell'intestazione finisce per compilare la propria copia implicita del modello per ogni diverso utilizzo dell'argomento del modello.
Ciò significa molto utilizzo inutile del disco e tempo di compilazione.
Ecco un esempio concreto, in cui entrambi main.cpp
e notmain.cpp
implicitamente definiscono a MyTemplate<int>
causa del suo utilizzo in quei file.
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
#endif
notmain.hpp
#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
int notmain();
#endif
GitHub a monte .
Compila e visualizza i simboli con nm
:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate
Produzione:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
Da man nm
, vediamo che W
significa simbolo debole, che GCC ha scelto perché questa è una funzione modello. Il simbolo debole significa che il codice compilato implicitamente generato per è MyTemplate<int>
stato compilato su entrambi i file.
Il motivo per cui non esplode al momento del collegamento con più definizioni è che il linker accetta più definizioni deboli e ne sceglie solo una da inserire nell'eseguibile finale.
I numeri nell'output significano:
0000000000000000
: indirizzo nella sezione. Questo zero è perché i modelli vengono inseriti automaticamente nella propria sezione
0000000000000017
: dimensione del codice generato per loro
Possiamo vederlo un po 'più chiaramente con:
objdump -S main.o | c++filt
che termina con:
Disassembly of section .text._ZN10MyTemplateIiE1fEi:
0000000000000000 <MyTemplate<int>::f(int)>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 89 75 f4 mov %esi,-0xc(%rbp)
f: 8b 45 f4 mov -0xc(%rbp),%eax
12: 83 c0 01 add $0x1,%eax
15: 5d pop %rbp
16: c3 retq
ed _ZN10MyTemplateIiE1fEi
è il nome mutilato di MyTemplate<int>::f(int)>
cui ha c++filt
deciso di non districare.
Quindi vediamo che una sezione separata viene generata per ogni singola istanza di metodo e che ognuna di esse occupa ovviamente spazio nei file oggetto.
Soluzioni al problema della ridefinizione degli oggetti
Questo problema può essere evitato utilizzando un'istanza esplicita e:
mantieni la definizione su hpp e aggiungi extern template
hpp per i tipi che verranno istanziati in modo esplicito.
Come spiegato in: l' uso di un modello esterno (C ++ 11) extern template
impedisce che un modello completamente definito venga istanziato dalle unità di compilazione, ad eccezione della nostra istanza esplicita. In questo modo, solo la nostra istanziazione esplicita sarà definita negli oggetti finali:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
extern template class MyTemplate<int>;
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
Svantaggio:
- se sei una libreria di sola intestazione, costringi i progetti esterni a fare la loro istanza esplicita. Se non sei una libreria di sola intestazione, questa soluzione è probabilmente la migliore.
- se il tipo di modelloèdefinito nel tuo progetto e non un built-in come
int
, sembra che tu sia costretto ad aggiungere l'inclusione nell'intestazione, una dichiarazione in avanti non è sufficiente: modello esterno e tipi incompleti Ciò aumenta le dipendenze dell'intestazione un po.
spostando la definizione sul file cpp, lasciare solo la dichiarazione su hpp, ovvero modificare l'esempio originale in modo che sia:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
template class MyTemplate<int>;
Svantaggio: i progetti esterni non possono utilizzare il tuo modello con i propri tipi. Inoltre sei costretto a istanziare esplicitamente tutti i tipi. Ma forse questo è un vantaggio da allora i programmatori non dimenticheranno.
mantieni la definizione su hpp e aggiungi extern template
ogni includer:
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
extern template class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Svantaggio: tutti gli includer devono aggiungere il extern
ai loro file CPP, cosa che probabilmente i programmatori dimenticheranno di fare.
Con una di queste soluzioni, nm
ora contiene:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
quindi vediamo che ha solo mytemplate.o
una compilazione di MyTemplate<int>
come desiderato, mentre notmain.o
e main.o
non perché U
significa indefinito.
Rimuovi le definizioni dalle intestazioni incluse ma esponi anche i modelli di un'API esterna in una libreria di sole intestazioni
Se la tua libreria non è solo intestazione, il extern template
metodo funzionerà, poiché l'utilizzo dei progetti si collegherà semplicemente al tuo file oggetto, che conterrà l'oggetto dell'istanza esplicita del modello.
Tuttavia, per le librerie di sola intestazione, se vuoi entrambe:
- velocizza la compilazione del tuo progetto
- esporre le intestazioni come API di libreria esterna per consentire ad altri di utilizzarle
quindi puoi provare uno dei seguenti:
-
mytemplate.hpp
: definizione del modello
mytemplate_interface.hpp
: dichiarazione del modello che corrisponde solo alle definizioni da mytemplate_interface.hpp
, nessuna definizione
mytemplate.cpp
: include mytemplate.hpp
e crea istanze esplicite
main.cpp
e ovunque nel codice base: include mytemplate_interface.hpp
, notmytemplate.hpp
-
mytemplate.hpp
: definizione del modello
mytemplate_implementation.hpp
: include mytemplate.hpp
e aggiunge extern
a ogni classe che verrà istanziata
mytemplate.cpp
: include mytemplate.hpp
e crea istanze esplicite
main.cpp
e ovunque nel codice base: include mytemplate_implementation.hpp
, notmytemplate.hpp
O ancora meglio forse per più intestazioni: crea una cartella intf
/ impl
all'interno della tua includes/
cartella e usa sempre mytemplate.hpp
come nome.
L' mytemplate_interface.hpp
approccio è simile a questo:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
#include "mytemplate_interface.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
#endif
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate_interface.hpp"
int main() {
std::cout << MyTemplate<int>().f(1) << std::endl;
}
Compila ed esegui:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o
Produzione:
2
Testato in Ubuntu 18.04.
Moduli C ++ 20
https://en.cppreference.com/w/cpp/language/modules
Penso che questa funzione fornirà la migliore configurazione in futuro non appena sarà disponibile, ma non l'ho ancora verificata perché non è ancora disponibile sul mio GCC 9.2.1.
Dovrai comunque eseguire un'istanza esplicita per ottenere l'accelerazione / il salvataggio del disco, ma almeno avremo una soluzione sensata per "Rimuovere le definizioni dalle intestazioni incluse ma anche esporre i modelli un'API esterna" che non richiede la copia di cose circa 100 volte.
L'utilizzo previsto (senza l'insantiazione esplicita, non sono sicuro di quale sarà la sintassi esatta, vedere: Come utilizzare l'istanza esplicita del modello con i moduli C ++ 20? ) Essere qualcosa lungo:
helloworld.cpp
export module helloworld;
import <iostream>;
template<class T>
export void hello(T t) {
std::cout << t << std::end;
}
main.cpp
import helloworld;
int main() {
hello(1);
hello("world");
}
e poi compilation menzionata su https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o
Quindi da questo vediamo che clang può estrarre l'interfaccia del modello + l'implementazione nella magia helloworld.pcm
, che deve contenere una rappresentazione intermedia LLVM del sorgente: come vengono gestiti i modelli nel sistema di moduli C ++? che consente ancora la specifica del modello.
Come analizzare rapidamente la tua build per vedere se guadagnerebbe molto dall'istanziazione del modello
Quindi, hai un progetto complesso e vuoi decidere se l'istanziazione del modello porterà vantaggi significativi senza effettivamente eseguire il refactoring completo?
L'analisi seguente potrebbe aiutarti a decidere, o almeno a selezionare gli oggetti più promettenti da sottoporre a refactoring durante la sperimentazione, prendendo in prestito alcune idee da: Il mio file oggetto C ++ è troppo grande
# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
grep ' W ' > nm.log
# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log
# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log
# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log
# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list.
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
sort -k1 -n > nm.gains.log
# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log
# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log
Il sogno: una cache del compilatore di modelli
Penso che la soluzione definitiva sarebbe se potessimo costruire con:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
e quindi myfile.o
riutilizzerebbe automaticamente i modelli precedentemente compilati tra i file.
Ciò significherebbe 0 sforzi in più per i programmatori oltre a passare quell'opzione CLI in più al tuo sistema di compilazione.
Un vantaggio secondario della creazione di istanze di modelli esplicite: aiuta gli IDE a elencare le istanze di modelli
Ho scoperto che alcuni IDE come Eclipse non possono risolvere "un elenco di tutte le istanze di modelli utilizzate".
Quindi, ad esempio, se ci si trova all'interno di un codice basato su modelli e si desidera trovare i possibili valori del modello, è necessario trovare gli usi del costruttore uno per uno e dedurre i possibili tipi uno per uno.
Ma su Eclipse 2020-03 posso facilmente elencare i modelli istanziati in modo esplicito eseguendo una ricerca Trova tutti gli usi (Ctrl + Alt + G) sul nome della classe, che mi punta ad esempio da:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
per:
template class AnimalTemplate<Dog>;
Ecco una demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Un'altra tecnica di guerriglia che potresti usare al di fuori dell'IDE, tuttavia, sarebbe quella di eseguire nm -C
sull'eseguibile finale e grep il nome del modello:
nm -C main.out | grep AnimalTemplate
che punta direttamente al fatto che Dog
era una delle istanze:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)