Szablony funkcji oraz szablony klas to potężne narzędzie do tworzenia uniwersalnego kodu. Dzięki nim możemy raz napisać algorytm, który potem będzie wykonywany dla wielu typów. Może się jednak okazać, że czas kompilacji takiego rozwiązania może być długi. W tym poście pokażę, jak można spróbować go skrócić z wykorzystaniem explicit instantiation.
Zacznijmy od prostego przykładu. Załóżmy, że posiadamy bibliotekę library.h
, która posiada funkcję szablonową library_function<T>()
. Funkcja ta jest używana w dwóch plikach: a.cpp
i b.cpp
, w każdym z typem int
.
template <typename T> T library_function(T arg) { return arg; }
int function_a(int x);
#include "a.h" #include "library.h" int function_a(int x) { return library_function(x) + 1; }
int function_b(int x);
#include "b.h" #include "library.h" int function_b(int x) { return library_function(x) + 2; }
#include "a.h" #include "b.h" #include <iostream> int main() { std::cout << (function_a(10) + function_b(20)) << std::endl; }
Przyjrzyjmy się dokładnie procesowi kompilacji tego małego projektu. Otóż zarówno podczas kompilacji pliku a.cpp
jak i b.cpp
nastąpi instancjacja funkcji szablonowej library_function<T>()
dla typu int
. Funkcja library_function<int>()
zostanie więc skompilowana dwukrotnie. Dopiero na etapie linkowania te dwie skompilowane funkcje zostaną zdeduplikowane i tylko jedna pojawi się w pliku wykonywalnym.
W naszym małym projekcie nie jest to problemem, ale łatwo wyobrazić sobie sytuację, w której funkcja library_function<int>()
jest używana w wielu jednostkach translacji, a więc tym samym wielokrotnie kompilowana będzie de facto ta sama funkcja.
Możemy temu zaradzić:
template <typename T> T library_function(T arg) { return arg; } extern template int library_function(int arg);
#include "library.h" template int library_function(int arg);
Za pomocą składni extern template
przekazujemy informację kompilatorowi, żeby sam nie instancjonował funkcji library_function<T>()
dla typu int
. Jednocześnie w nowym pliku library.cpp
instancjonujemy explicite tę funkcję dla tegoż typu. Dzięki temu uzyskujemy jedno miejsce, w którym nastąpi instancjacja i kompilacja funkcji, co może przyspieszyć czas kompilacji.
Możemy pójść jeszcze o krok dalej i przenieść ciało funkcji szablonowej z pliku nagłówkowego do pliku źródłowego. Dzięki temu we wszystkich plikach, które załączają nagłówek biblioteki nie będzie sprawdzana poprawność napisanej funkcji szablonowej, co może jeszcze bardziej przyspieszyć proces kompilacji. Z drugiej strony tracimy jednak możliwość użycia funkcji szablonowej dla innych typów niż te, dla których zainstancjonujemy szablon w pliku library.cpp
.
template <typename T> T library_function(T arg); extern template int library_function(int arg);
#include "library.h" template <typename T> T library_function(T arg) { return arg; } template int library_function(int arg);
W celu zobrazowania potencjalnego zysku przygotowałem większy projekt, w którym stworzyłem 400 funkcji bibliotecznych, które były wykorzystywane w 400 plikach źródłowych. Aby wydłużyć czas kompilacji pojedynczej funkcji bibliotecznej, każda funkcja wyliczała pewną wartość skomplikowanej rekurencyjnej funkcji w czasie kompilacji. Dokładny kod oraz kod generujący dostępne są na moim GitHubie. Poniżej prezentuję porównanie czasu kompilacji na mojej maszynie (1,8 GHz Dual-Core Intel Core i5, GCC 9.3.0).
Czas kompilacji | |
Bez explicit instancjacji | 2m34.874s |
Z explicit instancjacją | 1m0.017s |
Z explicit instancjacją i przeniesioną definicją do pliku źródłowego | 0m44.851s |
W ramach ciekawostki dodam, że kompilator Clang (11.0.0) wyraźnie gorzej radzi sobie niż GCC w tym zestawieniu. Poniżej czasy kompilacji dla wygenerowanego przykładu 100 funkcji użytych w 100 plikach.
Czas kompilacji GCC | Czas kompilacji Clang | |
Bez explicit instancjacji | 0m12.822s | 12m35.065s |
Z explicit instancjacją | 0m8.175s | 6m35.060s |
Z explicit instancjacją i przeniesioną definicją do pliku źródłowego | 0m7.779s | 0m11.986s |
Jak widać z uzyskanych rezultatów, użycie explicit instantiation może istotnie zredukować czas kompilacji. Oczywiście tak się stało w przypadku wygenerowanego przeze mnie projektu. W innych projektach rezultat nie musi być taki sam. Także czas działania aplikacji może się wydłużyć z powodu braku inliningu w podejściu z przeniesioną definicją do pliku źródłowego. Dlatego też najlepiej wykonać odpowiednie pomiary i porównać efekty w konkretnym projekcie. Udanych optymalizacji!
0 komentarzy