Jawne tworzenie instancji pozwala skrócić czas kompilacji i rozmiary obiektów
To są główne korzyści, jakie może przynieść. Pochodzą one z następujących dwóch efektów opisanych szczegółowo w poniższych sekcjach:
- usuń definicje z nagłówków, aby uniemożliwić narzędziom kompilacji przebudowę dołączeń (oszczędza czas)
- redefinicja obiektu (oszczędność czasu i rozmiaru)
Usuń definicje z nagłówków
Jawna instancja umożliwia pozostawienie definicji w pliku .cpp.
Gdy definicja znajduje się w nagłówku i zmodyfikujesz ją, inteligentny system kompilacji przekompilowałby wszystkie załączniki, które mogą składać się z dziesiątek plików, prawdopodobnie powodując nieznośnie powolną przyrostową rekompilację po zmianie pojedynczego pliku.
Umieszczanie definicji w plikach .cpp ma tę wadę, że biblioteki zewnętrzne nie mogą ponownie używać szablonu z własnymi nowymi klasami, ale „Usuń definicje z dołączonych nagłówków, ale także wyświetl szablony przez zewnętrzny interfejs API” poniżej pokazuje obejście tego problemu.
Zobacz konkretne przykłady poniżej.
Korzyści z redefinicji obiektu: zrozumienie problemu
Jeśli po prostu całkowicie zdefiniujesz szablon w pliku nagłówkowym, każda pojedyncza jednostka kompilacji, która zawiera ten nagłówek, będzie kompilować własną niejawną kopię szablonu dla każdego innego użycia argumentu szablonu.
Oznacza to dużo bezużytecznego wykorzystania dysku i czasu kompilacji.
Oto konkretny przykład, w którym zarówno main.cpp
i notmain.cpp
niejawnie definiują MyTemplate<int>
ze względu na użycie w tych plikach.
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 upstream .
Kompiluj i przeglądaj symbole za pomocą 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
Wynik:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
Od man nm
widzimy, że W
dochodowe słaby symbol, który wybrał GCC, ponieważ jest to funkcja szablonu.
Powodem, dla którego nie wysadza się w czasie łączenia z wieloma definicjami jest to, że linker akceptuje wiele słabych definicji i po prostu wybiera jedną z nich do umieszczenia w ostatecznym pliku wykonywalnym, a wszystkie są takie same w naszym przypadku, więc wszystko jest w porządku.
Liczby w danych wyjściowych oznaczają:
0000000000000000
: adres w sekcji. To zero wynika z tego, że szablony są automatycznie umieszczane we własnej sekcji
0000000000000017
: rozmiar wygenerowanego dla nich kodu
Widzimy to nieco wyraźniej dzięki:
objdump -S main.o | c++filt
która kończy się:
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
i _ZN10MyTemplateIiE1fEi
jest zniekształconą nazwą, MyTemplate<int>::f(int)>
której c++filt
zdecydowano się nie rozplątywać.
Widzimy więc, że dla każdej instancji metody generowana jest osobna sekcja i że każda z nich zajmuje oczywiście miejsce w plikach obiektowych.
Rozwiązania problemu redefinicji obiektu
Tego problemu można uniknąć, używając jawnej instancji i:
zachowaj definicję na hpp i dodaj extern template
hpp dla typów, które mają być jawnie tworzone.
Jak wyjaśniono w: użycie szablonu extern (C ++ 11) extern template
zapobiega tworzeniu wystąpienia całkowicie zdefiniowanego szablonu przez jednostki kompilacyjne, z wyjątkiem naszej jawnej instancji. W ten sposób tylko nasza jawna instancja zostanie zdefiniowana w końcowych obiektach:
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); }
Wady:
- definicja pozostaje w nagłówku, co powoduje, że pojedyncza zmiana pliku rekompiluje się do tego nagłówka prawdopodobnie wolno
- jeśli jesteś biblioteką tylko z nagłówkiem, zmuszasz zewnętrzne projekty do wykonania własnych, jawnych instancji. Jeśli nie jesteś biblioteką tylko z nagłówkami, to rozwiązanie jest prawdopodobnie najlepsze.
- jeśli typ szablonu jest zdefiniowany w twoim własnym projekcie, a nie wbudowanym podobnym
int
, wydaje się że jesteś zmuszony dodać do niego dołączenie do nagłówka, deklaracja do przodu nie wystarczy: extern szablon i niekompletne typy Zwiększa to zależności nagłówków trochę.
przenosząc definicję do pliku cpp, zostaw tylko deklarację na hpp, czyli zmodyfikuj oryginalny przykład na:
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>;
Wada: projekty zewnętrzne nie mogą używać Twojego szablonu z własnymi typami. Ponadto jesteś zmuszony jawnie tworzyć instancje wszystkich typów. Ale może to jest plus, ponieważ wtedy programiści nie zapomną.
zachowaj definicję na hpp i dodaj extern template
do każdego elementu włączającego:
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); }
Wada: wszystkie włączniki muszą dodać rozszerzenie extern
do swoich plików CPP, o czym programiści prawdopodobnie zapomną.
Każde z tych rozwiązań nm
zawiera teraz:
notmain.o
U MyTemplate<int>::f(int)
main.o
U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)
więc widzimy, że ma tylko mytemplate.o
kompilację MyTemplate<int>
zgodnie z życzeniem, podczas gdy notmain.o
i main.o
nie, ponieważ U
oznacza niezdefiniowany.
Usuń definicje z dołączonych nagłówków, ale także ujawnij szablony zewnętrzny interfejs API w bibliotece tylko z nagłówkami
Jeśli twoja biblioteka nie jest tylko nagłówkiem, plik extern template
metoda zadziała, ponieważ użycie projektów będzie po prostu linkować do twojego pliku obiektowego, który będzie zawierał obiekt jawnej instancji szablonu.
Jednak w przypadku bibliotek obsługujących tylko nagłówki, jeśli chcesz:
- przyspiesz kompilację swojego projektu
- udostępniać nagłówki jako zewnętrzny interfejs API biblioteki, aby inni mogli z niego korzystać
następnie możesz wypróbować jedną z następujących czynności:
-
mytemplate.hpp
: definicja szablonu
mytemplate_interface.hpp
: deklaracja szablonu pasująca tylko do definicji z mytemplate_interface.hpp
, brak definicji
mytemplate.cpp
: dołącz mytemplate.hpp
i utwórz jawne instancje
main.cpp
i wszędzie indziej w bazie kodu: dołącz mytemplate_interface.hpp
, niemytemplate.hpp
-
mytemplate.hpp
: definicja szablonu
mytemplate_implementation.hpp
: obejmuje mytemplate.hpp
i dodaje extern
do każdej klasy, której wystąpienie zostanie utworzone
mytemplate.cpp
: dołącz mytemplate.hpp
i utwórz jawne instancje
main.cpp
i wszędzie indziej w bazie kodu: dołącz mytemplate_implementation.hpp
, niemytemplate.hpp
A może nawet lepiej dla wielu nagłówków: utwórz folder intf
/ impl
w swoim includes/
folderze i używaj mytemplate.hpp
jako nazwy zawsze.
mytemplate_interface.hpp
Podejście wygląda następująco:
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;
}
Skompiluj i uruchom:
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
Wynik:
2
Testowane w Ubuntu 18.04.
C ++ 20 modułów
https://en.cppreference.com/w/cpp/language/modules
Myślę, że ta funkcja zapewni najlepszą konfigurację w przyszłości, gdy stanie się dostępna, ale jeszcze jej nie sprawdzałem, ponieważ nie jest jeszcze dostępna w moim GCC 9.2.1.
Nadal będziesz musiał wykonać jawną instancję, aby uzyskać przyspieszenie / zapisanie dysku, ale przynajmniej będziemy mieć rozsądne rozwiązanie „Usuń definicje z dołączonych nagłówków, ale także udostępnisz szablony zewnętrzne API”, które nie wymaga kopiowania około 100 razy.
Oczekiwane użycie (bez jawnej insantacji, nie jestem pewien, jaka będzie dokładna składnia, zobacz: Jak używać jawnej instancji szablonu z modułami C ++ 20? )
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");
}
a następnie kompilacja wspomniana na 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
Z tego widzimy, że clang może wyodrębnić interfejs szablonu + implementację do magii helloworld.pcm
, która musi zawierać pewną pośrednią reprezentację źródła LLVM: Jak obsługiwane są szablony w systemie modułów C ++?co nadal pozwala na specyfikację szablonu.
Jak szybko przeanalizować swoją kompilację, aby sprawdzić, czy wiele zyskałaby na instancji szablonu
Masz więc złożony projekt i chcesz zdecydować, czy tworzenie instancji szablonu przyniesie znaczące korzyści bez faktycznego wykonywania pełnego refaktora?
Poniższa analiza może pomóc Ci zdecydować lub przynajmniej wybrać najbardziej obiecujące obiekty do refaktoryzacji w pierwszej kolejności podczas eksperymentowania, zapożyczając kilka pomysłów z: Mój plik obiektowy C ++ jest za duży
# 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
Marzenie: pamięć podręczna kompilatora szablonów
Myślę, że najlepszym rozwiązaniem byłoby budowanie z:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
a następnie myfile.o
automatycznie ponownie używał wcześniej skompilowanych szablonów w różnych plikach.
Oznaczałoby to 0 dodatkowego wysiłku dla programistów poza przekazaniem tej dodatkowej opcji CLI do twojego systemu kompilacji.
Dodatkowa premia w postaci jawnej instancji szablonu: pomoc w wyświetlaniu instancji szablonów przez środowiska IDE
Zauważyłem, że niektóre środowiska IDE, takie jak Eclipse, nie potrafią rozpoznać „listy wszystkich używanych wystąpień szablonów”.
Na przykład, jeśli jesteś w kodzie opartym na szablonie i chcesz znaleźć możliwe wartości szablonu, musisz znaleźć konstruktor, który używa jednego po drugim i wydedukować możliwe typy jeden po drugim.
Ale w Eclipse 2020-03 mogę łatwo wyświetlić listę szablonów z jawną instancją, wykonując wyszukiwanie Znajdź wszystkie zastosowania (Ctrl + Alt + G) na nazwie klasy, co wskazuje mi np. Z:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
do:
template class AnimalTemplate<Dog>;
Oto demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Inną techniką guerrila, której możesz użyć poza IDE, byłoby uruchomienie nm -C
na końcowym pliku wykonywalnym i grep z nazwą szablonu:
nm -C main.out | grep AnimalTemplate
co bezpośrednio wskazuje na fakt, że Dog
była to jedna z instancji:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)