Durch explizite Instanziierung können Kompilierungszeiten und Objektgrößen reduziert werden
Dies sind die Hauptgewinne, die es bieten kann. Sie stammen aus den folgenden zwei Effekten, die in den folgenden Abschnitten ausführlich beschrieben werden:
- Entfernen Sie Definitionen aus den Headern, um zu verhindern, dass Build-Tools Includer neu erstellen
- Objektneudefinition
Entfernen Sie Definitionen aus den Headern
Durch explizite Instanziierung können Sie Definitionen in der CPP-Datei belassen.
Wenn sich die Definition im Header befindet und Sie sie ändern, kompiliert ein intelligentes Build-System alle Einschlüsse neu. Dies können Dutzende von Dateien sein, was die Kompilierung unerträglich langsam macht.
Das Einfügen von Definitionen in CPP-Dateien hat den Nachteil, dass externe Bibliotheken die Vorlage nicht mit ihren eigenen neuen Klassen wiederverwenden können. Die folgende Option enthält jedoch eine Problemumgehung.
Siehe konkrete Beispiele unten.
Objektneudefinition gewinnt: das Problem verstehen
Wenn Sie eine Vorlage in einer Header-Datei nur vollständig definieren, kompiliert jede einzelne Kompilierungseinheit, die diesen Header enthält, eine eigene implizite Kopie der Vorlage für jede andere verwendete Verwendung von Vorlagenargumenten.
Dies bedeutet viel nutzlose Festplattennutzung und Kompilierungszeit.
Hier ist ein konkretes Beispiel, in dem beide main.cpp
und aufgrund ihrer Verwendung in diesen Dateien notmain.cpp
implizit definiert MyTemplate<int>
werden.
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 stromaufwärts .
Kompilieren und Anzeigen von Symbolen mit 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
Ausgabe:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
Aus sehen man nm
wir, dass W
dies ein schwaches Symbol bedeutet, das GCC gewählt hat, weil dies eine Vorlagenfunktion ist. Schwaches Symbol bedeutet, dass der kompilierte implizit generierte Code für MyTemplate<int>
beide Dateien kompiliert wurde.
Der Grund, warum es bei der Verknüpfung mit mehreren Definitionen nicht explodiert, ist, dass der Linker mehrere schwache Definitionen akzeptiert und nur eine davon auswählt, um sie in die endgültige ausführbare Datei einzufügen.
Die Zahlen in der Ausgabe bedeuten:
0000000000000000
: Adresse innerhalb des Abschnitts. Diese Null liegt daran, dass Vorlagen automatisch in einen eigenen Abschnitt eingefügt werden
0000000000000017
: Größe des für sie generierten Codes
Wir können dies etwas deutlicher sehen mit:
objdump -S main.o | c++filt
was endet in:
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
und _ZN10MyTemplateIiE1fEi
ist der verstümmelte Name, von MyTemplate<int>::f(int)>
dem c++filt
entschieden wurde, nicht zu entwirren.
Wir sehen also, dass für jede einzelne Methodeninstanziierung ein separater Abschnitt generiert wird und dass jeder von ihnen natürlich Platz in den Objektdateien beansprucht.
Lösungen für das Problem der Objektneudefinition
Dieses Problem kann durch explizite Instanziierung vermieden werden.
Verschieben Sie die Definition in die CPP-Datei, lassen Sie nur die Deklaration in HPP, dh ändern Sie das ursprüngliche Beispiel wie folgt:
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; }
// Explicit instantiation.
template class MyTemplate<int>;
Nachteil: Externe Projekte können Ihre Vorlage nicht mit eigenen Typen verwenden. Außerdem müssen Sie alle Typen explizit instanziieren. Aber vielleicht ist dies ein Vorteil, da Programmierer es dann nicht vergessen werden.
Behalten Sie die Definition auf hpp bei und fügen Sie extern template
jeden Einschluss hinzu, siehe auch: Verwenden einer externen Vorlage (C ++ 11), dh ändern Sie das ursprüngliche Beispiel wie folgt :
mytemplate.cpp
#include "mytemplate.hpp"
// Explicit instantiation.
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
// extern template declaration
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 declaration
extern template class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Nachteil: Alle Einschließer müssen das extern
zu ihren CPP-Dateien hinzufügen , was Programmierer wahrscheinlich vergessen werden.
Behalten Sie die Definition auf hpp bei und fügen Sie extern template
hpp für Typen hinzu, die explizit instanziiert werden sollen:
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"
// Explicit instantiation required just for int.
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); }
Nachteil: Sie zwingen externe Projekte, ihre eigene explizite Instanziierung durchzuführen.
Mit einer dieser Lösungen nm
enthält jetzt:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
so sehen wir haben haben nur mytemplate.o
eine zusammenstellung von MyTemplate<int>
wie gewünscht, während notmain.o
und main.o
nicht weil U
bedeutet undefiniert.
Entfernen Sie Definitionen aus den enthaltenen Headern, machen Sie aber auch Vorlagen für eine externe API verfügbar
Schließlich gibt es noch einen Anwendungsfall, den Sie berücksichtigen sollten, wenn Sie beides möchten:
- Beschleunigen Sie die Kompilierung Ihres Projekts
- Stellen Sie Header als externe Bibliotheks-API bereit, damit andere sie verwenden können
Um dies zu lösen, können Sie eine der folgenden Aktionen ausführen:
-
mytemplate.hpp
: Vorlagendefinition
mytemplate_interface.hpp
: Vorlagendeklaration, die nur mit den Definitionen von übereinstimmt mytemplate_interface.hpp
, keine Definitionen
mytemplate.cpp
: mytemplate.hpp
explizite Instantiierungen einschließen und vornehmen
main.cpp
und überall sonst in der Codebasis: einschließen mytemplate_interface.hpp
, nichtmytemplate.hpp
-
mytemplate.hpp
: Vorlagendefinition
mytemplate_implementation.hpp
: schließt jede Klasse ein mytemplate.hpp
und fügt extern
sie hinzu , die instanziiert wird
mytemplate.cpp
: mytemplate.hpp
explizite Instantiierungen einschließen und vornehmen
main.cpp
und überall sonst in der Codebasis: einschließen mytemplate_implementation.hpp
, nichtmytemplate.hpp
Oder noch besser für mehrere Header: Erstellen Sie einen intf
/ impl
Ordner in Ihrem includes/
Ordner und verwenden Sie ihn immer mytemplate.hpp
als Namen.
Der mytemplate_interface.hpp
Ansatz sieht folgendermaßen aus:
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"
// Explicit instantiation.
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate_interface.hpp"
int main() {
std::cout << MyTemplate<int>().f(1) << std::endl;
}
Kompilieren und ausführen:
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
Ausgabe:
2
Getestet in Ubuntu 18.04.
C ++ 20 Module
https://en.cppreference.com/w/cpp/language/modules
Ich denke, diese Funktion bietet das beste Setup für die Zukunft, sobald sie verfügbar ist, aber ich habe sie noch nicht überprüft, da sie auf meinem GCC 9.2.1 noch nicht verfügbar ist.
Sie müssen noch eine explizite Instanziierung durchführen, um die Geschwindigkeit zu erhöhen / die Festplatte zu speichern, aber zumindest haben wir eine vernünftige Lösung für "Entfernen von Definitionen aus enthaltenen Headern, aber auch Anzeigen einer externen API für Vorlagen", bei der das Kopieren nicht etwa 100 Mal erforderlich ist.
Die erwartete Verwendung (ohne die explizite Begründung, nicht sicher, wie die genaue Syntax aussehen wird, siehe: Wie wird die explizite Instanziierung von Vorlagen mit C ++ 20-Modulen verwendet? )
helloworld.cpp
export module helloworld; // module declaration
import <iostream>; // import declaration
template<class T>
export void hello(T t) { // export declaration
std::cout << t << std::end;
}
main.cpp
import helloworld; // import declaration
int main() {
hello(1);
hello("world");
}
und dann die unter https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/ erwähnte Zusammenstellung.
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
Daraus sehen wir, dass clang die Template-Schnittstelle + Implementierung in die Magie extrahieren kann helloworld.pcm
, die eine LLVM-Zwischendarstellung der Quelle enthalten muss: Wie werden Vorlagen im C ++ - Modulsystem behandelt? Dies ermöglicht weiterhin die Vorlage von Vorlagenspezifikationen.
So analysieren Sie Ihren Build schnell, um festzustellen, ob die Vorlageninstanziierung viel bringt
Sie haben also ein komplexes Projekt und möchten entscheiden, ob die Instanziierung von Vorlagen erhebliche Vorteile bringt, ohne den vollständigen Refactor auszuführen?
Die folgende Analyse kann Ihnen dabei helfen, die vielversprechendsten Objekte zu bestimmen oder zumindest auszuwählen, die beim Experimentieren zuerst umgestaltet werden sollen, indem Sie einige Ideen ausleihen: Meine C ++ - Objektdatei ist zu groß
# 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
Der Traum: ein Template-Compiler-Cache
Ich denke, die ultimative Lösung wäre, wenn wir bauen könnten mit:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
und myfile.o
würde dann zuvor zuvor kompilierte Vorlagen automatisch für mehrere Dateien wiederverwenden.
Dies würde 0 zusätzlichen Aufwand für die Programmierer bedeuten, abgesehen davon, dass diese zusätzliche CLI-Option an Ihr Build-System übergeben wird.