Udostępnij za pośrednictwem


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 __assumeprogramie 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ść.

Zobacz też