Najlepsze rozwiązania dotyczące optymalizacji
W tym dokumencie opisano niektóre najlepsze rozwiązania dotyczące optymalizowania programów C++ w programie Visual Studio.
Opcje kompilatora i konsolidatora
Optymalizacja sterowana profilem
Program Visual Studio obsługuje optymalizację sterowaną profilem (PGO). Ta optymalizacja wykorzystuje dane profilu z wykonywania trenowania instrumentowanej wersji aplikacji w celu późniejszej optymalizacji aplikacji. Korzystanie z infrastruktury PGO może być czasochłonne, więc może nie być czymś, czego używa każdy deweloper, ale zalecamy użycie PGO do ostatniej kompilacji wydania produktu. Aby uzyskać więcej informacji, zobacz Profile-Guided Optimizations (Optymalizacje sterowane profilem).
Ponadto optymalizacja całego programu (wie również jako generowanie kodu czasu połączenia) i /O1
/O2
optymalizacje zostały ulepszone. Ogólnie rzecz biorąc, aplikacja skompilowana przy użyciu jednej z tych opcji będzie szybsza niż ta sama aplikacja skompilowana przy użyciu wcześniejszego kompilatora.
Aby uzyskać więcej informacji, zobacz (Optymalizacja całego programu) i/O1
, /O2
(Minimalizuj rozmiar, Maksymalizuj szybkość)./GL
Który poziom optymalizacji do użycia
Jeśli w ogóle to możliwe, ostateczne kompilacje wydania powinny być kompilowane przy użyciu optymalizacji z przewodnikiem profilu. Jeśli nie jest możliwe kompilowanie przy użyciu PGO, czy z powodu niewystarczającej infrastruktury do uruchamiania instrumentowanych kompilacji lub braku dostępu do scenariuszy, sugerujemy utworzenie za pomocą optymalizacji całego programu.
Przełącznik /Gy
jest również bardzo przydatny. Generuje on osobną wartość COMDAT dla każdej funkcji, zapewniając konsolidatorowi większą elastyczność w przypadku usuwania nieużywanych comDATs i składania COMDAT. Jedyną wadą używania /Gy
jest to, że może to powodować problemy podczas debugowania. W związku z tym zaleca się używanie go. Aby uzyskać więcej informacji, zobacz /Gy
(Włączanie łączenia na poziomie funkcji).
W przypadku łączenia w środowiskach 64-bitowych zaleca się użycie /OPT:REF,ICF
opcji konsolidatora, a w środowiskach 32-bitowych /OPT:REF
zaleca się użycie opcji konsolidatora. Aby uzyskać więcej informacji, zobacz /OPT (Optymalizacje).
Zdecydowanie zaleca się również generowanie symboli debugowania, nawet w przypadku zoptymalizowanych kompilacji wydania. Nie ma to wpływu na wygenerowany kod i znacznie ułatwia debugowanie aplikacji, jeśli jest to konieczne.
Przełączniki zmiennoprzecinkowe
/Op
Opcja kompilatora została usunięta i dodano następujące cztery opcje kompilatora do obsługi optymalizacji zmiennoprzecinkowych:
Opcja | Opis |
---|---|
/fp:precise |
Jest to zalecenie domyślne i powinno być używane w większości przypadków. |
/fp:fast |
Zalecane, jeśli wydajność ma największe znaczenie, na przykład w grach. Spowoduje to najszybszą wydajność. |
/fp:strict |
Zalecane, jeśli wymagane jest dokładne wyjątki zmiennoprzecinkowe i zachowanie IEEE. Spowoduje to najwolniejsze działanie. |
/fp:except[-] |
Można użyć w połączeniu z lub /fp:strict /fp:precise , ale nie /fp:fast . |
Aby uzyskać więcej informacji, zobacz /fp
(Określ zachowanie zmiennoprzecinkowe).
Declspecs optymalizacji
W tej sekcji przyjrzymy się dwóm declspecs, które mogą być używane w programach w celu ułatwienia wydajności: __declspec(restrict)
i __declspec(noalias)
.
Declspec restrict
można zastosować tylko do deklaracji funkcji, które zwracają wskaźnik, na przykład __declspec(restrict) void *malloc(size_t size);
Declspec restrict
jest używany w funkcjach, które zwracają niealiasowane wskaźniki. To słowo kluczowe jest używane do implementacji malloc
biblioteki języka C-Runtime, ponieważ nigdy nie zwróci wartości wskaźnika, która jest już używana w bieżącym programie (chyba że robisz coś nielegalnego, takiego jak użycie pamięci po jego uwolnieniu).
Declspec restrict
udostępnia kompilatorowi więcej informacji na temat przeprowadzania optymalizacji kompilatora. Jedną z najtrudniejszych rzeczy dla kompilatora do określenia jest to, co wskaźniki aliasują inne wskaźniki, a użycie tych informacji znacznie pomaga kompilatorowi.
Warto zwrócić uwagę, że jest to obietnica kompilatora, a nie coś, co kompilator zweryfikuje. Jeśli program używa tego restrict
declspec niewłaściwie, program może mieć nieprawidłowe zachowanie.
Aby uzyskać więcej informacji, zobacz restrict
.
Declspec noalias
jest również stosowany tylko do funkcji i wskazuje, że funkcja jest funkcją półczystą. Funkcja półczysta to funkcja, która odwołuje się lub modyfikuje tylko ustawienia lokalne, argumenty i pośredniości pierwszego poziomu argumentów. Ten declspec jest obietnicą kompilatora, a jeśli funkcja odwołuje się do globalnych lub pośrednich drugiego poziomu argumentów wskaźnika, kompilator może wygenerować kod, który przerywa działanie aplikacji.
Aby uzyskać więcej informacji, zobacz noalias
.
Pragmas optymalizacji
Istnieje również kilka przydatnych pragma do ułatwienia optymalizacji kodu. Pierwsza omówiona przez nas opcja to #pragma optimize
:
#pragma optimize("{opt-list}", on | off)
Ta pragma umożliwia ustawienie danego poziomu optymalizacji na podstawie funkcji po funkcji. Jest to idealne rozwiązanie w przypadku tych rzadkich przypadków, w których aplikacja ulega awarii, gdy dana funkcja jest kompilowana z optymalizacją. Za pomocą tej funkcji można wyłączyć optymalizacje dla jednej funkcji:
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
Aby uzyskać więcej informacji, zobacz optimize
.
Inlining jest jedną z najważniejszych optymalizacji, które kompilator wykonuje, a tutaj mówimy o kilku pragmas, które pomagają zmodyfikować to zachowanie.
#pragma inline_recursion
jest przydatna do określania, czy aplikacja ma mieć możliwość śródliniowego wywołania cyklicznego. Domyślnie jest wyłączona. W przypadku płytkiej rekursji małych funkcji można to włączyć. Aby uzyskać więcej informacji, zobacz inline_recursion
.
Inną przydatną pragma do ograniczania głębokości podkreślenia jest #pragma inline_depth
. Jest to zazwyczaj przydatne w sytuacjach, w których próbujesz ograniczyć rozmiar programu lub funkcji. Aby uzyskać więcej informacji, zobacz inline_depth
.
__restrict
i __assume
W programie Visual Studio istnieje kilka słów kluczowych, które mogą pomóc w wydajności: __restrict i __assume.
Po pierwsze należy zauważyć, że __restrict
i __declspec(restrict)
są dwiema różnymi rzeczami. Chociaż są one nieco powiązane, ich semantyka jest inna. __restrict
jest kwalifikatorem typu, na przykład const
lub volatile
, ale wyłącznie dla typów wskaźników.
Wskaźnik, który jest modyfikowany __restrict
za pomocą , jest określany jako wskaźnik __restrict. Wskaźnik __restrict to wskaźnik, do którego można uzyskać dostęp tylko za pośrednictwem wskaźnika __restrict. Innymi słowy, inny wskaźnik nie może być używany do uzyskiwania dostępu do danych wskazywanych przez wskaźnik __restrict.
__restrict
może być zaawansowanym narzędziem optymalizatora języka Microsoft C++, ale używać go z dużą starannością. W przypadku nieprawidłowego użycia optymalizator może wykonać optymalizację, która spowoduje przerwanie działania aplikacji.
W __assume
programie deweloper może poinformować kompilator o założeniu wartości jakiejś zmiennej.
Na przykład __assume(a < 5);
informuje optymalizator, że w tym wierszu kodu zmienna a
jest mniejsza niż 5. Ponownie jest to obietnica kompilatora. Jeśli a
w tym momencie program ma wartość 6, zachowanie programu po zoptymalizowaniu kompilatora może nie być oczekiwane. __assume
jest najbardziej przydatna przed instrukcjami switch i/lub wyrażeniami warunkowymi.
Istnieją pewne ograniczenia dotyczące usługi __assume
. Najpierw, podobnie jak __restrict
, jest to tylko sugestia, więc kompilator może go zignorować. Ponadto obecnie __assume
działa tylko ze zmiennymi nierównościami w stosunku do stałych. Nie propaguje ona na przykład nierówności symbolicznych, na przykład przyjmij (b < ).
Obsługa wewnętrzna
Funkcje wewnętrzne to wywołania funkcji, w których kompilator ma wewnętrzną wiedzę na temat wywołania i zamiast wywoływać funkcję w bibliotece, emituje kod dla tej funkcji. Plik <nagłówka intrin.h> zawiera wszystkie dostępne funkcje wewnętrzne dla każdej z obsługiwanych platform sprzętowych.
Funkcje wewnętrzne zapewniają programistom możliwość zagłębiania się w kod bez konieczności używania zestawu. Korzystanie z funkcji wewnętrznych ma kilka zalet:
Twój kod jest bardziej przenośny. Kilka funkcji wewnętrznych jest dostępnych w wielu architekturach procesora.
Kod jest łatwiejszy do odczytania, ponieważ kod jest nadal zapisywany w języku C/C++.
Twój kod uzyskuje korzyści z optymalizacji kompilatora. W miarę ulepszania kompilatora poprawia się generowanie kodu dla funkcji wewnętrznych.
Aby uzyskać więcej informacji, zobacz Funkcje wewnętrzne kompilatora.
Wyjątki
Występuje trafienie wydajności związane z używaniem wyjątków. Niektóre ograniczenia są wprowadzane podczas korzystania z bloków try, które hamują działanie kompilatora przed wykonaniem pewnych optymalizacji. Na platformach x86 występuje dodatkowe obniżenie wydajności z bloków try ze względu na dodatkowe informacje o stanie, które należy wygenerować podczas wykonywania kodu. Na platformach 64-bitowych bloki try nie obniżają wydajności, ale gdy zostanie zgłoszony wyjątek, proces znajdowania programu obsługi i odwijania stosu może być kosztowny.
Dlatego zaleca się unikanie wprowadzania bloków try/catch do kodu, który naprawdę go nie potrzebuje. Jeśli musisz użyć wyjątków, w miarę możliwości użyj wyjątków synchronicznych. Aby uzyskać więcej informacji, zobacz Obsługa wyjątków strukturalnych (C/C++).
Na koniec należy zgłaszać wyjątki tylko w wyjątkowych przypadkach. Użycie wyjątków dla ogólnego przepływu sterowania prawdopodobnie wpłynie na wydajność.