Jakich technik można użyć do przyspieszenia czasu kompilacji C ++?

249

Jakich technik można użyć do przyspieszenia czasu kompilacji C ++?

To pytanie pojawiło się w kilku komentarzach do stylu programowania przepełnienia stosu C ++ i jestem zainteresowany usłyszeniem, jakie są pomysły.

Widziałem podobne pytanie: Dlaczego kompilacja w C ++ trwa tak długo? , ale to nie zapewnia wielu rozwiązań.

Scott Langham
źródło
1
Czy możesz podać nam kontekst? A może szukasz bardzo ogólnych odpowiedzi?
Pirolistyczny
1
Bardzo podobne do tego pytania: stackoverflow.com/questions/364240/…
Adam Rosenfield
Ogólne odpowiedzi. Mam naprawdę dużą bazę kodu napisaną przez wiele osób. Pomysły na atak, które byłyby dobre. Interesujące byłyby również sugestie dotyczące szybkiego kompilowania nowo napisanego kodu.
Scott Langham,
Zauważ, że często istotnym elementem czasie kompilacji nie jest używana przez kompilator, ale przez skrypty kompilacji
thi gg
1
Przejrzałem tę stronę i nie widziałem żadnych wzmianek o pomiarach. Napisałem mały skrypt powłoki, który dodaje znacznik czasu do każdego otrzymywanego wiersza, dzięki czemu mogę po prostu wprowadzić wywołanie „make”. To pozwala mi zobaczyć, które cele są najdroższe, całkowity czas kompilacji lub łącza itp. Po prostu przez porównanie znaczników czasu. Jeśli spróbujesz tego podejścia, pamiętaj, że znaczniki czasu będą niedokładne w przypadku kompilacji równoległych.
John P

Odpowiedzi:

257

Techniki językowe

Idiom Pimpl

Spójrz na idiom Pimpl tutaj i tutaj , znany również jako nieprzezroczysty wskaźnik lub klasy uchwytów. Nie tylko przyspiesza kompilację, ale także zwiększa bezpieczeństwo wyjątków w połączeniu z funkcją wymiany bez rzucania . Idiom Pimpl pozwala zmniejszyć zależności między nagłówkami i zmniejsza liczbę ponownych kompilacji, które należy wykonać.

Przekazywanie deklaracji

Tam, gdzie to możliwe, używaj deklaracji forward . Jeśli kompilator musi tylko wiedzieć, że SomeIdentifierjest to struktura, wskaźnik lub cokolwiek innego, nie dołączaj całej definicji, zmuszając kompilator do wykonania większej pracy, niż jest to konieczne. Może to mieć efekt kaskadowy, powodując, że droga będzie wolniejsza niż powinna.

Te I / O strumienie są szczególnie znane spowolnienie budowli. Jeśli potrzebujesz ich w pliku nagłówkowym, spróbuj #include <iosfwd>zamiast <iostream>i #include <iostream>tylko w pliku implementacyjnym. <iosfwd>Nagłówka posiada deklaracji tylko do przodu. Niestety inne standardowe nagłówki nie mają odpowiedniego nagłówka deklaracji.

Preferuj przekazywanie przez odniesienie zamiast przekazywania w podpisach funkcji. Eliminuje to konieczność # włączenia odpowiednich definicji typów w pliku nagłówkowym, a jedynie deklarowanie typu jest konieczne. Oczywiście preferuj stałe odwołania zamiast nie stałych, aby uniknąć niejasnych błędów, ale jest to kwestia dotycząca innego pytania.

Warunki ochrony

Zastosuj warunki ochrony, aby pliki nagłówkowe nie były dołączane więcej niż raz w jednej jednostce tłumaczeniowej.

#pragma once
#ifndef filename_h
#define filename_h

// Header declarations / definitions

#endif

Używając zarówno pragmy, jak i ifndef, uzyskujesz przenośność prostego rozwiązania makr, a także optymalizację prędkości kompilacji, którą niektóre kompilatory mogą zrobić w obecności pragma oncedyrektywy.

Zmniejsz współzależność

Im bardziej modułowy i mniej współzależny jest twój projekt kodu, tym rzadziej będziesz musiał wszystko kompilować. Możesz także w końcu zmniejszyć ilość pracy, jaką kompilator musi wykonać na dowolnym pojedynczym bloku w tym samym czasie, ponieważ ma mniej do kontrolowania.

Opcje kompilatora

Wstępnie skompilowane nagłówki

Służą do jednokrotnego skompilowania wspólnej sekcji dołączonych nagłówków dla wielu jednostek tłumaczeniowych. Kompilator kompiluje go raz i zapisuje swój stan wewnętrzny. Ten stan można następnie szybko załadować, aby szybko rozpocząć kompilowanie innego pliku z tym samym zestawem nagłówków.

Uważaj, abyś zawierał tylko rzadko zmieniane elementy we wstępnie skompilowanych nagłówkach, w przeciwnym razie możesz często wykonywać pełne przebudowy częściej niż to konieczne. Jest to dobre miejsce dla nagłówków STL i innych plików dołączanych do biblioteki.

ccache to kolejne narzędzie, które wykorzystuje techniki buforowania w celu przyspieszenia działania.

Użyj równoległości

Wiele kompilatorów / IDE obsługuje jednoczesne wykonywanie wielu rdzeni / procesorów. W GNU Make (zwykle używanym z GCC) użyj -j [N]opcji. W programie Visual Studio w preferencjach dostępna jest opcja umożliwiająca jednoczesne tworzenie wielu projektów. Możesz także skorzystać z /MPopcji paralelizmu na poziomie pliku zamiast paralelizmu na poziomie projektu.

Inne narzędzia równoległe:

Użyj niższego poziomu optymalizacji

Im bardziej kompilator próbuje zoptymalizować, tym trudniej musi działać.

Udostępnione biblioteki

Przeniesienie rzadziej modyfikowanego kodu do bibliotek może skrócić czas kompilacji. Korzystając z bibliotek współdzielonych ( .solub .dll), możesz również skrócić czas łączenia.

Zdobądź szybszy komputer

Więcej pamięci RAM, szybsze dyski twarde (w tym dyski SSD) i więcej procesorów / rdzeni będzie miało wpływ na szybkość kompilacji.

Zaćmienie
źródło
11
Wstępnie skompilowane nagłówki nie są jednak idealne. Efektem ubocznym ich używania jest to, że dostajesz więcej plików niż to konieczne (ponieważ każda jednostka kompilacyjna używa tego samego nagłówka prekompilowanego), co może wymuszać pełne ponowne kompilowanie częściej niż to konieczne. Tylko coś do zapamiętania.
czerwiec
8
W nowoczesnych kompilatorach #ifndef jest tak samo szybki jak #pragma jeden raz (pod warunkiem, że osłona dołączenia znajduje się na górze pliku). Więc nie ma korzyści #pragma raz w kategoriach kompilacji łącze
jalf
7
Nawet jeśli masz tylko VS 2005, a nie 2008, możesz dodać przełącznik / MP w opcjach kompilacji, aby umożliwić równoległe budowanie na poziomie .cpp.
macbirdie
6
Dyski SSD były zbyt drogie, kiedy napisano tę odpowiedź, ale dziś są najlepszym wyborem przy kompilacji C ++. Podczas kompilacji uzyskujesz dostęp do wielu małych plików. Wymaga to dużo IOPS, które dostarczają dyski SSD.
MSalters
14
Preferuj przekazywanie przez odniesienie zamiast przekazywania w podpisach funkcji. Wyeliminuje to potrzebę #include odpowiednich definicji typów w pliku nagłówkowym. To źle , nie musisz mieć pełnego typu, aby zadeklarować funkcję, która przechodzi przez wartość, potrzebujesz tylko pełnego typu, aby zaimplementować lub użyć tej funkcji , ale w większości przypadków (chyba że przekierowujesz tylko połączenia) i tak będziesz potrzebować tej definicji.
David Rodríguez - dribeas
43

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ą:

  1. Musisz się upewnić, że prekompilowane pliki nagłówkowe są stabilne i nie będą się zmieniać ( zawsze możesz zmienić swój plik makefile )
  2. 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 .

Mani Zandifar
źródło
33

Poleciłbym te artykuły z „Games from Within, Indie Game Design And Programming”:

To prawda, że ​​są dość stare - trzeba będzie ponownie przetestować wszystko z najnowszymi wersjami (lub dostępnymi wersjami), aby uzyskać realistyczne wyniki. Tak czy inaczej, jest to dobre źródło pomysłów.

Paulius
źródło
17

Jedna technika, która w przeszłości działała dla mnie całkiem dobrze: nie kompiluj wielu plików źródłowych C ++ niezależnie, ale raczej wygeneruj jeden plik C ++, który zawiera wszystkie pozostałe pliki, taki jak ten:

// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"

Oczywiście oznacza to, że musisz ponownie skompilować cały dołączony kod źródłowy w przypadku zmiany któregokolwiek ze źródeł, więc drzewo zależności pogarsza się. Jednak kompilowanie wielu plików źródłowych jako jednej jednostki tłumaczeniowej jest szybsze (przynajmniej w moich eksperymentach z MSVC i GCC) i generuje mniejsze pliki binarne. Podejrzewam również, że kompilator ma większy potencjał optymalizacji (ponieważ może zobaczyć więcej kodu na raz).

Ta technika psuje się w różnych przypadkach; na przykład, kompilator wyskoczy, gdy dwa lub więcej plików źródłowych zadeklaruje funkcję globalną o tej samej nazwie. Nie mogłem jednak znaleźć tej techniki opisanej w żadnej z pozostałych odpowiedzi, dlatego właśnie tutaj ją wspominam.

Co do tego jest warte, Projekt KDE zastosował tę samą technikę od 1999 roku, aby zbudować zoptymalizowane pliki binarne (być może w wersji). Wywołano przejście do skryptu konfiguracji kompilacji --enable-final. Ze względów archeologicznych wykopałem publikację, która ogłosiła tę funkcję: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2

Frerich Raabe
źródło
2
Nie jestem pewien, czy to naprawdę to samo, ale myślę, że włączenie „Optymalizacji całego programu” w VC ++ ( msdn.microsoft.com/en-us/library/0zza0de8%28VS.71%29.aspx ) powinno mieć taki sam wpływ na wydajność środowiska wykonawczego, jak sugerujesz. Jednak czas kompilacji może być zdecydowanie lepszy!
Philipp
1
@Frerich: Opisujesz wersje Unity wymienione w odpowiedzi OJ. Widziałem je również zwane kompilacjami zbiorczymi i kompilacjami wzorcowymi.
idbrii
Jak więc UB wypada w porównaniu z WPO / LTCG?
paulm
Jest to potencjalnie przydatne tylko w przypadku jednorazowych kompilacji, a nie podczas programowania, w którym przełączasz się między edycją, budowaniem i testowaniem. We współczesnym świecie normą są cztery rdzenie, może kilka lat później liczba rdzeni jest znacznie większa. Jeśli kompilator i konsolidator nie są w stanie korzystać z wielu wątków, lista plików może być podzielona na <core-count> + Nlisty podrzędne, które są kompilowane równolegle, gdzie Njest odpowiednia liczba całkowita (w zależności od pamięci systemowej i sposobu, w jaki maszyna jest używana).
FooF,
15

Jest cała książka na ten temat, zatytułowana Large-Scale C ++ Software Design (napisana przez Johna Lakosa).

Książka zawiera wcześniejsze szablony, więc do treści tej książki dodaj „użycie szablonów może spowolnić kompilator”.

ChrisW
źródło
Książka jest często przywoływana w tego rodzaju tematach, ale dla mnie była rzadka w informacji. Zasadniczo stwierdza, że ​​należy używać deklaracji przesyłania w jak największym stopniu i rozdzielać zależności. Jest to nieco oczywiste poza tym, że używanie idiomu pimpl ma wady czasu wykonywania.
gast128
@ gast128 Myślę, że jego celem jest użycie idiomów kodowania, które pozwalają na przyrostową ponowną kompilację, tzn. aby zmienić gdzieś trochę źródła, nie trzeba będzie wszystko kompilować.
ChrisW
15

Po prostu odsyłam do mojej innej odpowiedzi: Jak TY skrócisz czas kompilacji i czas łączenia dla projektów Visual C ++ (natywne C ++)? . Kolejną kwestią, którą chcę dodać, ale która często powoduje problemy, jest użycie prekompilowanych nagłówków. Ale proszę, używaj ich tylko do części, które prawie nigdy się nie zmieniają (takich jak nagłówki narzędzi GUI). W przeciwnym razie będą cię kosztować więcej czasu niż zaoszczędzą w końcu.

Inną opcją jest, aby podczas pracy z GNU make włączyć -j<N>opcję:

  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

Zwykle mam go, 3odkąd mam tutaj podwójny rdzeń. Następnie uruchomi równolegle kompilatory dla różnych jednostek tłumaczeniowych, pod warunkiem, że nie będzie między nimi zależności. Łączenia nie można wykonać równolegle, ponieważ istnieje tylko jeden proces łączący łączący ze sobą wszystkie pliki obiektowe.

Ale sam linker może być wątkowy i to właśnie robi linker ELF . Jest to zoptymalizowany wątkowy kod C ++, o którym mówi się, że łączy pliki obiektowe ELF o wielkość szybciej niż stary (i faktycznie został zawarty w binutils ).GNU gold ld

Johannes Schaub - litb
źródło
Ok tak. Przepraszam, że to pytanie nie pojawiło się podczas wyszukiwania.
Scott Langham,
nie musiałeś żałować. to było dla Visual C ++. twoje pytanie wydaje się dotyczyć dowolnego kompilatora. więc w porządku :)
Johannes Schaub - litb
12

Oto niektóre:

  • Użyj wszystkich rdzeni procesora, uruchamiając zadanie wielokrotnej kompilacji ( make -j2jest to dobry przykład).
  • Wyłącz lub obniż optymalizacje (na przykład GCC działa znacznie szybciej -O1niż -O2lub -O3).
  • Użyj prekompilowanych nagłówków .
Milan Babuškov
źródło
12
Dla twojej informacji, zwykle szybsze jest uruchomienie większej liczby procesów niż rdzeni. Na przykład w systemie czterordzeniowym zwykle używam -j8, a nie -j4. Powodem tego jest to, że gdy jeden proces jest blokowany we / wy, drugi może się kompilować.
Pan Fooz,
@MrFooz: Testowałem to kilka lat temu, kompilując jądro Linuksa (z pamięci RAM) na i7-2700k (4 rdzenie, 8 wątków, ustawiłem stały mnożnik). Zapominam dokładnie najlepszy wynik, ale -j12dookoła -j18były znacznie szybsze niż -j8, tak jak sugerujesz. Zastanawiam się, ile rdzeni możesz mieć, zanim przepustowość pamięci stanie się czynnikiem ograniczającym ...
Mark K Cowan
@MarkKCowan zależy to od wielu czynników. Różne komputery mają bardzo różne pasma pamięci. W dzisiejszych procesorach wysokiej klasy nasycenie magistrali pamięci wymaga wielu rdzeni. Ponadto istnieje równowaga między I / O a CPU. Niektóre kody są bardzo łatwe do skompilowania, inne mogą być wolne (np. Z dużą ilością szablonów). Moja obecna zasada polega na -jdwukrotności liczby rzeczywistych rdzeni.
Pan Fooz,
11

Po zastosowaniu wszystkich powyższych sztuczek kodu (deklaracje do przodu, ograniczenie włączania nagłówków do minimum w nagłówkach publicznych, wypychanie większości szczegółów w pliku implementacji za pomocą Pimpl ...) i nic więcej nie można uzyskać pod względem językowym, rozważ swój system kompilacji . Jeśli używasz Linuksa, rozważ użycie distcc (kompilator rozproszony) i ccache (kompilator pamięci podręcznej).

Pierwszy, distcc, wykonuje krok preprocesora lokalnie, a następnie wysyła dane wyjściowe do pierwszego dostępnego kompilatora w sieci. Wymaga tej samej wersji kompilatora i biblioteki we wszystkich skonfigurowanych węzłach w sieci.

Ta ostatnia, ccache, jest pamięcią podręczną kompilatora. Ponownie wykonuje preprocesor, a następnie sprawdza za pomocą wewnętrznej bazy danych (przechowywanej w katalogu lokalnym), czy ten plik preprocesora został już skompilowany z tymi samymi parametrami kompilatora. Jeśli tak, po prostu wyświetla plik binarny i dane wyjściowe z pierwszego uruchomienia kompilatora.

Oba mogą być używane jednocześnie, więc jeśli ccache nie ma lokalnej kopii, może wysłać ją przez sieć do innego węzła za pomocą distcc, lub może po prostu wstrzyknąć rozwiązanie bez dalszego przetwarzania.

David Rodríguez - dribeas
źródło
2
Nie sądzę, że distcc wymaga tych samych wersji bibliotek na wszystkich skonfigurowanych węzłach. distcc wykonuje kompilację tylko zdalnie, a nie linkowanie. Wysyła również wstępnie przetworzony kod za pośrednictwem przewodu, więc nagłówki dostępne w systemie zdalnym nie mają znaczenia.
Frerich Raabe
9

Kiedy wyszłam ze studiów, pierwszy prawdziwy kod C ++, który był warty produkcji, miał te tajemne dyrektywy #ifndef ... #endif między nimi, w których zdefiniowano nagłówki. Zapytałem faceta, który pisał kod o tych nadrzędnych rzeczach w bardzo naiwny sposób i zostałem wprowadzony do świata programowania na dużą skalę.

Wracając do sedna, używanie dyrektyw w celu zapobiegania powielaniu definicji nagłówków było pierwszą rzeczą, której nauczyłem się, jeśli chodzi o skrócenie czasu kompilacji.

questzen
źródło
1
stary ale jary. czasami oczywiste jest zapomniane.
alcor
1
„uwzględnij strażników”
gast128
8

Więcej pamięci RAM.

Ktoś mówił o dyskach RAM w innej odpowiedzi. Zrobiłem to z 80286 i Turbo C ++ (pokazuje wiek), a wyniki były fenomenalne. Podobnie jak utrata danych, gdy maszyna uległa awarii.

kalendarz mr
źródło
w DOS-ie nie można mieć dużo pamięci
phuclv
6

W miarę możliwości używaj deklaracji przesyłania dalej. Jeśli deklaracja klasy używa tylko wskaźnika lub odwołania do typu, możesz po prostu zadeklarować go dalej i dołączyć nagłówek typu do pliku implementacji.

Na przykład:

// T.h
class Class2; // Forward declaration

class T {
public:
    void doSomething(Class2 &c2);
private:
    Class2 *m_Class2Ptr;
};

// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
    // Whatever you want here
}

Mniej obejmuje oznacza znacznie mniej pracy preprocesora, jeśli zrobisz to wystarczająco.

Evan Teran
źródło
Czy to nie ma znaczenia tylko wtedy, gdy ten sam nagłówek jest zawarty w kilku jednostkach tłumaczeniowych? Jeśli istnieje tylko jedna jednostka tłumacząca (jak to często bywa przy użyciu szablonów), nie wydaje się, aby miało to wpływ.
AlwaysLearning
1
Jeśli jest tylko jedna jednostka tłumacząca, po co zawracać sobie głowę jej umieszczaniem w nagłówku? Czy nie ma większego sensu umieszczanie zawartości w pliku źródłowym? Czy nie chodzi o to, że nagłówki mogą zawierać więcej niż jeden plik źródłowy?
Evan Teran
5

Posługiwać się

#pragma once

u góry plików nagłówkowych, więc jeśli są one dołączone więcej niż raz w jednostce tłumaczącej, tekst nagłówka zostanie uwzględniony i parsowany tylko raz.

Scott Langham
źródło
2
Chociaż powszechnie obsługiwany, #pragma raz jest niestandardowy. Zobacz en.wikipedia.org/wiki/Pragma_once
ChrisInEdmonton
7
A w dzisiejszych czasach zwykłe osłony obejmują ten sam efekt. Tak długo, jak są na górze pliku, kompilator jest w stanie w pełni traktować je jako #pragma raz
08:30
5

Dla kompletności: kompilacja może być powolna, ponieważ system kompilacji jest głupi, a także ponieważ kompilator zajmuje dużo czasu.

Przeczytaj Rekursywne czyń uważane za szkodliwe (PDF), aby omówić ten temat w środowiskach uniksowych.

dmckee --- były kot moderator
źródło
4
  • Zaktualizuj komputer

    1. Zdobądź quad core (lub system dual-quad)
    2. Uzyskaj DUŻO RAM.
    3. Użyj napędu RAM, aby radykalnie zmniejszyć opóźnienia we / wy pliku. (Są firmy, które produkują dyski IDE i SATA RAM, które działają jak dyski twarde).
  • Następnie masz wszystkie inne typowe sugestie

    1. Użyj wstępnie skompilowanych nagłówków, jeśli są dostępne.
    2. Zmniejsz ilość sprzężenia między częściami twojego projektu. Zmiana jednego pliku nagłówka zwykle nie powinna wymagać ponownej kompilacji całego projektu.
Uhall
źródło
4

Miałem pomysł na użycie napędu RAM . Okazało się, że w moich projektach wcale nie robi to żadnej różnicy. Ale wciąż są dość małe. Spróbuj! Chciałbym usłyszeć, jak bardzo to pomogło.

Vilx-
źródło
Huh Dlaczego ktoś głosował za tym? Spróbuję jutro.
Scott Langham,
1
Spodziewam się, że głosowanie jest takie, ponieważ nigdy nie robi dużej różnicy. Jeśli masz wystarczającą ilość nieużywanej pamięci RAM, system operacyjny i tak inteligentnie wykorzysta ją jako pamięć podręczną dysku.
MSalters
1
@MSalters - a ile byłoby „wystarczające”? Wiem, że to jest teoria, ale z jakiegoś powodu przy użyciu RAMdrive ma faktycznie dać znaczący impuls. Idź rysunek ...
Vilx-
1
wystarczy skompilować projekt i nadal buforować pliki wejściowe i tymczasowe. Oczywiście strona w GB będzie zależeć bezpośrednio od wielkości twojego projektu. Należy zauważyć, że w starszych systemach operacyjnych (w szczególności WinXP) pamięci podręczne plików były dość leniwe, pozostawiając pamięć RAM nieużywaną.
MSalters
Z pewnością napęd RAM jest szybszy, jeśli pliki są już w pamięci RAM, a nie najpierw wykonują całą masę powolnych operacji we / wy, a następnie są w pamięci RAM? (powtórz wzrost dla plików, które się zmieniły - zapisz je z powrotem na dysk itp.).
paulm
3

Łączenie dynamiczne (.so) może być znacznie szybsze niż łączenie statyczne (.a). Zwłaszcza, gdy masz wolny dysk sieciowy. Dzieje się tak, ponieważ masz cały kod w pliku .a, który należy przetworzyć i zapisać. Ponadto na dysk należy zapisać znacznie większy plik wykonywalny.


źródło
dynamiczne łączenie zapobiega wielu rodzajom optymalizacji czasu łącza, więc w wielu przypadkach dane wyjściowe mogą być wolniejsze
phuclv
3

Nie o czasie kompilacji, ale o czasie kompilacji:

  • Użyj ccache, jeśli musisz odbudować te same pliki, gdy pracujesz nad plikami build

  • Użyj kompilacji ninja zamiast make. Obecnie kompiluję projekt z ~ 100 plikami źródłowymi i wszystko jest buforowane przez ccache. potrzeba 5 minut, ninja mniej niż 1.

Możesz wygenerować swoje pliki ninja z cmake za pomocą -GNinja.

thg gg
źródło
3

Gdzie spędzasz czas? Czy jesteś związany procesorem? Pamięć związana? Dysk związany? Czy możesz użyć więcej rdzeni? Więcej pamięci RAM? Potrzebujesz RAID? Czy chcesz po prostu poprawić wydajność swojego obecnego systemu?

Czy pod gcc / g ++ spojrzałeś na ccache ? Może to być pomocne, jeśli make clean; makedużo robisz .

Mr.Ree
źródło
2

Szybsze dyski twarde.

Kompilatory zapisują na dysku wiele (i być może ogromne) pliki. Praca z dyskiem SSD zamiast typowego dysku twardego i czasy kompilacji są znacznie krótsze.

linello
źródło
2

W Linuksie (i być może w niektórych innych * NIXach) możesz naprawdę przyspieszyć kompilację, NIE ZATRZYMUJĄC wyjścia i przechodząc na inną TTY.

Oto eksperyment: printf spowalnia mój program

Flawiusz
źródło
2

Udziały sieciowe drastycznie spowalniają twoją kompilację, ponieważ opóźnienia wyszukiwania są wysokie. W przypadku czegoś takiego jak Boost, zrobiło to dla mnie ogromną różnicę, mimo że nasz dysk sieciowy jest dość szybki. Czas na skompilowanie programu doładowania zabawek skrócił się z około 1 minuty do 1 sekundy, kiedy przestawiłem się z udziału sieciowego na lokalny dysk SSD.

Mark Lakata
źródło
2

Jeśli masz procesor wielordzeniowy, zarówno Visual Studio (2005 i nowsze wersje), jak i GCC obsługują kompilacje wieloprocesorowe. Na pewno można to włączyć, jeśli masz sprzęt.

Peter Mortensen
źródło
2
@Fellman, Zobacz niektóre inne odpowiedzi - użyj opcji -j #.
strager
1

Chociaż nie jest to „technika”, nie mogłem rozgryźć, w jaki sposób Win32 projektuje z wieloma plikami źródłowymi kompilowanymi szybciej niż mój pusty projekt „Hello World”. Mam więc nadzieję, że to pomogło komuś takiemu, jak ja.

W programie Visual Studio jedną z opcji zwiększania czasu kompilacji jest Łączenie przyrostowe ( / INCREMENTAL ). Jest niezgodny z generowaniem kodu czasu łącza ( / LTCG ), więc pamiętaj o wyłączeniu przyrostowego łączenia podczas kompilacji wersji.

Nathan Goings
źródło
1
wyłączenie generowania kodu czasu łącza nie jest dobrą sugestią, ponieważ wyłącza wiele optymalizacji. Musisz włączyć tylko /INCREMENTALw trybie debugowania
phuclv,
1

Począwszy od Visual Studio 2017, możesz mieć pewne dane kompilatora dotyczące tego, co wymaga czasu.

Dodaj te parametry do C / C ++ -> Wiersz polecenia (Opcje dodatkowe) w oknie właściwości projektu: /Bt+ /d2cgsummary /d1reportTime

Możesz mieć więcej informacji w tym poście .

Xavier Bigand
źródło
0

Użycie dynamicznego linkowania zamiast statycznego powoduje, że kompilator jest szybszy, co można wyczuć.

Jeśli używasz t Cmake, aktywuj właściwość:

set(BUILD_SHARED_LIBS ON)

Wersja kompilacji, użycie statycznego linkowania może jeszcze bardziej zoptymalizować.

Cheng
źródło