Pracuję nad projektem STAPL, który jest silnie szablonowaną biblioteką C ++. Raz na jakiś czas musimy ponownie zapoznać się ze wszystkimi technikami, aby skrócić czas kompilacji. Tutaj streściłem stosowane przez nas techniki. Niektóre z tych technik są już wymienione powyżej:
Znajdowanie najbardziej czasochłonnych sekcji
Chociaż nie ma udowodnionej korelacji między długością symboli a czasem kompilacji, zauważyliśmy, że mniejsze średnie rozmiary symboli mogą poprawić czas kompilacji we wszystkich kompilatorach. Twoim pierwszym celem jest znalezienie największych symboli w kodzie.
Metoda 1 - Sortuj symbole według rozmiaru
Możesz użyć nmpolecenia, aby wyświetlić symbole na podstawie ich rozmiarów:
nm --print-size --size-sort --radix=d YOUR_BINARY
W tym poleceniu --radix=dpozwala zobaczyć rozmiary w liczbach dziesiętnych (domyślnie jest to hex). Teraz, patrząc na największy symbol, zidentyfikuj, czy możesz rozbić odpowiednią klasę i spróbuj przeprojektować ją, łącząc nieszablonowe części w klasie bazowej lub dzieląc klasę na wiele klas.
Metoda 2 - Sortuj symbole według długości
Możesz uruchomić normalnie nm polecenie i przesłać je do swojego ulubionego skryptu ( AWK , Python itp.), Aby posortować symbole na podstawie ich długości . Opierając się na naszym doświadczeniu, ta metoda identyfikuje największe problemy czyniące kandydatów lepszymi niż metoda 1.
Metoda 3 - Użyj Templight
„ Templight to narzędzie oparte na Clang do profilowania czasu i zużycia pamięci przez instancje szablonów oraz do wykonywania interaktywnych sesji debugowania w celu uzyskania introspekcji w procesie tworzenia szablonów”.
Możesz zainstalować Templight, sprawdzając LLVM i Clang ( instrukcje ) i stosując na nim łatkę Templight. Domyślne ustawienia dla LLVM i Clang dotyczą debugowania i asercji, które mogą znacząco wpłynąć na czas kompilacji. Wygląda na to, że Templight potrzebuje obu, więc musisz użyć ustawień domyślnych. Proces instalacji LLVM i Clanga powinien zająć około godziny.
Po zastosowaniu poprawki możesz użyć templight++kompilacji kodu znajdującego się w folderze kompilacji określonym podczas instalacji.
Upewnij się, że templight++jest to w ŚCIEŻCE. Teraz, aby skompilować, dodaj następujące przełączniki do swojego CXXFLAGSw Makefile lub do opcji wiersza poleceń:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Lub
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Po zakończeniu kompilacji będziesz mieć .trace.memory.pbf i .trace.pbf wygenerowane w tym samym folderze. Aby wizualizować te ślady, możesz użyć Narzędzi Templight, które mogą przekonwertować je na inne formaty. Postępuj zgodnie z tymi instrukcjami, aby zainstalować konwerter templight. Zwykle używamy wyjścia callgrind. Możesz również użyć wyjścia GraphViz, jeśli twój projekt jest mały:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
Wygenerowany plik callgrind można otworzyć za pomocą kcachegrind, w którym można śledzić tworzenie instancji o największym czasie / pamięci.
Zmniejszenie liczby instancji szablonów
Chociaż nie ma dokładnego rozwiązania w celu zmniejszenia liczby instancji szablonów, istnieje kilka wskazówek, które mogą pomóc:
Refaktoryzuj klasy z więcej niż jednym argumentem szablonu
Na przykład, jeśli masz zajęcia,
template <typename T, typename U>
struct foo { };
i zarówno TiU może mieć 10 różnych opcji, to zwiększyła możliwe dawałaby szablonów tej klasy 100. Jednym ze sposobów rozwiązania tego problemu jest abstrakcyjny wspólna część kodu do innej klasy. Inną metodą jest użycie inwersji dziedziczenia (odwrócenie hierarchii klas), ale należy upewnić się, że cele projektowe nie zostaną naruszone przed użyciem tej techniki.
Przekieruj kod bez szablonów na poszczególne jednostki tłumaczeniowe
Korzystając z tej techniki, możesz raz skompilować wspólną sekcję i połączyć ją z innymi JT (jednostkami tłumaczeniowymi) później.
Używaj instancji zewnętrznych szablonów (od C ++ 11)
Jeśli znasz wszystkie możliwe wystąpienia klasy, możesz użyć tej techniki do skompilowania wszystkich przypadków w innej jednostce tłumaczeniowej.
Na przykład w:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Wiemy, że ta klasa może mieć trzy możliwe instancje:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Umieść powyższe w jednostce tłumaczeniowej i użyj słowa kluczowego extern w pliku nagłówkowym, poniżej definicji klasy:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Ta technika pozwala zaoszczędzić czas, jeśli kompilujesz różne testy ze wspólnym zestawem instancji.
UWAGA: MPICH2 ignoruje jawne tworzenie instancji w tym momencie i zawsze kompiluje instowane klasy we wszystkich jednostkach kompilacji.
Używaj kompilacji jedności
Ideą kompilacji jedności jest włączenie wszystkich plików .cc, których używasz w jednym pliku, i skompilowanie tego pliku tylko raz. Korzystając z tej metody, można uniknąć przywracania wspólnych sekcji różnych plików, a jeśli projekt zawiera wiele wspólnych plików, prawdopodobnie zaoszczędziłby również dostęp do dysku.
Jako przykład, załóżmy, masz trzy pliki foo1.cc, foo2.cc, foo3.cca wszystkie one należą tuplez STL . Możesz utworzyć foo-all.ccwyglądający następująco:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Kompilujesz ten plik tylko raz i potencjalnie redukujesz typowe wystąpienia trzech plików. Ogólnie trudno jest przewidzieć, czy poprawa może być znacząca, czy nie. Ale jednym oczywistym faktem jest to, że straciłbyś równoległość w swoich kompilacjach (nie możesz już kompilować trzech plików jednocześnie).
Ponadto, jeśli którykolwiek z tych plików zajmie dużo pamięci, możesz faktycznie zabraknąć pamięci przed zakończeniem kompilacji. W niektórych kompilatorach, takich jak GCC , może to powodować ICE (wewnętrzny błąd kompilatora) z powodu braku pamięci. Więc nie używaj tej techniki, chyba że znasz wszystkie zalety i wady.
Wstępnie skompilowane nagłówki
Wstępnie skompilowane nagłówki (PCH) mogą zaoszczędzić dużo czasu podczas kompilacji, kompilując pliki nagłówkowe do pośredniej reprezentacji rozpoznawalnej przez kompilator. Aby wygenerować wstępnie skompilowane pliki nagłówkowe, wystarczy skompilować plik nagłówkowy za pomocą zwykłego polecenia kompilacji. Na przykład w GCC:
$ g++ YOUR_HEADER.hpp
Spowoduje to wygenerowanie YOUR_HEADER.hpp.gch file( .gchto rozszerzenie plików PCH w GCC) w tym samym folderze. Oznacza to, że jeśli uwzględniszYOUR_HEADER.hpp do jakiegoś innego pliku, kompilator użyje twojego YOUR_HEADER.hpp.gchzamiast YOUR_HEADER.hppw tym samym folderze wcześniej.
Istnieją dwie problemy z tą techniką:
- Musisz się upewnić, że prekompilowane pliki nagłówkowe są stabilne i nie będą się zmieniać ( zawsze możesz zmienić swój plik makefile )
- Możesz dołączyć tylko jeden PCH na jednostkę kompilacji (w większości kompilatorów). Oznacza to, że jeśli masz więcej niż jeden plik nagłówkowy do prekompilacji, musisz dołączyć je do jednego pliku (np
all-my-headers.hpp.). Ale to oznacza, że musisz dołączyć nowy plik we wszystkich miejscach. Na szczęście GCC ma rozwiązanie tego problemu. Posługiwać się-include i podaj nowy plik nagłówka. Za pomocą tej techniki możesz przecinać różne pliki.
Na przykład:
g++ foo.cc -include all-my-headers.hpp
Używaj nienazwanych lub anonimowych przestrzeni nazw
Nienazwane przestrzenie nazw (inaczej anonimowe przestrzenie nazw) mogą znacznie zmniejszyć generowane rozmiary binarne. Nienazwane przestrzenie nazw wykorzystują wewnętrzne powiązanie, co oznacza, że symbole generowane w tych przestrzeniach nazw nie będą widoczne dla innych JT (jednostki translacji lub kompilacji). Kompilatory zwykle generują unikalne nazwy dla nienazwanych przestrzeni nazw. Oznacza to, że jeśli masz plik foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
I zdarza się, że dołączasz ten plik do dwóch JT (dwa pliki .cc i kompilujesz je osobno). Dwa wystąpienia szablonu foo nie będą takie same. Narusza to zasadę jednej definicji (ODR). Z tego samego powodu w plikach nagłówkowych nie zaleca się używania nienazwanych przestrzeni nazw. Używaj ich w swoich .ccplikach, aby uniknąć pojawiania się symboli w plikach binarnych. W niektórych przypadkach zmiana wszystkich wewnętrznych szczegółów .ccpliku wykazała 10% zmniejszenie generowanych rozmiarów binarnych.
Zmiana opcji widoczności
W nowszych kompilatorach możesz wybrać symbole, które będą widoczne lub niewidoczne w dynamicznych obiektach współdzielonych (DSO). Idealnie, zmiana widoczności może poprawić wydajność kompilatora, optymalizować czas łącza (LTO) i generować rozmiary binarne. Jeśli spojrzysz na pliki nagłówkowe STL w GCC, zobaczysz, że jest on powszechnie używany. Aby włączyć opcje widoczności, musisz zmienić kod na funkcję, na klasę, na zmienną i, co ważniejsze, na kompilator.
Za pomocą widoczności możesz ukryć symbole, które uważasz za prywatne, przed wygenerowanymi obiektami współdzielonymi. W GCC możesz kontrolować widoczność symboli, przekazując domyślną lub ukrytą -visibilityopcję kompilatora. Jest to w pewnym sensie podobne do nienazwanej przestrzeni nazw, ale w bardziej wyszukany i nachalny sposób.
Jeśli chcesz określić widoczności dla każdego przypadku, musisz dodać następujące atrybuty do swoich funkcji, zmiennych i klas:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
Domyślna widoczność w GCC jest domyślna (publiczna), co oznacza, że jeśli skompilujesz powyższą -sharedmetodę jako bibliotekę współdzieloną ( ), foo2a klasa foo3nie będzie widoczna w innych JT ( foo1i foo4będzie widoczna). Jeśli się skompilujesz, -visibility=hiddentylko foo1widoczne będą. Nawet foo4byłby ukryty.
Możesz przeczytać więcej o widoczności na GCC wiki .