Najlepsze praktyki optymalizacji
W tym dokumencie opisano niektóre najważniejsze wskazówki dotyczące optymalizacji w programie Visual C++.Omówiono następujące tematy:
Opcje konsolidatora i kompilatora
Profilowana optymalizacja
Którego poziomu optymalizacji powinienem użyć?
Przełączniki liczb zmiennoprzecinkowych
Optymalizacja Declspecs
Optymalizacja Pragm
__restrict i __assume
Wsparcie wewnętrzne
Wyjątki
Opcje konsolidatora i kompilatora
Profilowana optymalizacja
Visual C++ obsługuje profilowaną optymalizację (PGO).Ta optymalizacja korzysta z danych profilu z ostatnich wywołań instrumentowanych wersji aplikacji, aby zastosować optymalizacje aplikacji później.Używanie PGO może być czasochłonne, więc nie może być czymś, czego używa każdy deweloper, ale zaleca się używania PGO podczas ostatecznej kompilacji produktu.Aby uzyskać dodatkowe informacje, zobacz Optymalizacje sterowane profilem.
Dodatkowo, cała optymalizacja programu (znana również jako łącze generowania kodu czasu) i optymalizacje /O1 i /O2 zostały ulepszone.Ogólnie rzecz biorąc, aplikacja skompilowana z jedną z tych opcji będzie szybsza niż ta sama aplikacja skompilowana z wcześniejszą wersją kompilatora.
Aby uzyskać więcej informacji, zobacz /GL (Optymalizacja całego programu) i /O1, /O2 (Minimalizuj rozmiar, maksymalizuj szybkość).
Którego poziomu optymalizacji powinienem użyć?
Jeśli to w ogóle możliwe, ostateczne skompilowane wydanie powinno być skompilowane z Profilowaną optymalizacją.Jeśli kompilacja z PGO nie jest możliwa, z powodu niewystarczającej infrastruktury dla uruchomienia instrumentowanej kompilacji lub z powodu braku dostępu do scenariuszy, zalecamy kompilacje z optymalizacją całego programu.
Przełącznik /Gy jest również bardzo przydatny.Generuje on oddzielne COMDAT dla każdej funkcji, dając konsolidatorowi większą elastyczność, jeśli chodzi o usuwanie nieużywanych składni COMDATs i COMDAT.Jedyną wadą używania /Gy jest to, że może mieć on niewielki wpływ na czas kompilacji.Dlatego ogólnie zaleca się go używać.Aby uzyskać dodatkowe informacje, zobacz /Gy (Włączenie łączenia na poziomie funkcji).
Do łączenia w środowiskach 64-bitowych, zaleca się stosowanie opcji konsolidatora /OPT:REF,ICF, w 32-bitowych środowiskach zaleca się używać /OPT:REF.Aby uzyskać dodatkowe informacje, zobacz /OPT (Optymalizacje).
Zdecydowanie zaleca się również generowanie symboli debugowania, nawet w przypadku optymalizacji kompilacja wydania.Nie ma to wpływu na wygenerowany kod, a sprawia, że w razie potrzeby dużo łatwiej jest debugować aplikacje.
Przełączniki liczb zmiennoprzecinkowych
Opcja kompilatora /Op została usunięta i dodano następujące cztery opcje kompilatora do radzenia sobie z optymalizacją liczb zmiennoprzecinkowych:
/fp:precise |
Jest to opcja domyślnie zalecana i powinno się ją stosować w większości przypadków. |
/fp:fast |
Zalecane, jeśli wydajność jest sprawą najwyższej wagi, na przykład w grach.Rezultatem będzie najszybsza wydajność. |
/fp:strict |
Zalecane jeśli wyjątki liczb zmiennoprzecinkowych i zachowanie IEEE jest pożądane.Rezultatem będzie wolniejsza wydajność. |
/fp:except[-] |
Mogą być używane w połączeniu z /fp:strict lub /fp:precise, ale nie z /fp:fast. |
Aby uzyskać dodatkowe informacje, zobacz /fp (Określenie zachowania zmiennoprzecinkowego).
Optymalizacja Declspecs
W tej sekcji przyjrzymy się dwóm declspecs używanych w programach w celu poprawienia wydajności: __declspec(restrict) i __declspec(noalias).
Declspec restrict może być stosowany tylko do deklaracji funkcji, które zwracają wskaźnik, taki jak __declspec(restrict) void *malloc(size_t size);
Declspec restrict jest używany w funkcjach zwracających wskaźniki nie posiadające aliasu.To słowo kluczowe jest używane do implementacji biblioteki C-Runtime malloc , ponieważ nigdy nie zwróci wartości wskaźnika, który jest już używany w bieżącym programie (chyba że zrobisz coś niedozwolonego, takiego jak wykorzystanie pamięci po jej zwolnieniu).
Declspec restrict daje kompilatorowi więcej informacji do wykonywania optymalizacji kompilatora.Jedną z najtrudniejszych rzeczy dla kompilatora jest ustalenie, które wskaźniki są aliasem pozostałych wskaźników, wykorzystanie tych informacji znacznie pomaga kompilatorowi.
Warto wskazać, że jest to tylko zobowiązanie do kompilatora, nie coś, co kompilator będzie weryfikował.Jeśli program używa declspec restrict niewłaściwie, może zachowywać się nieprawidłowo.
Aby uzyskać dodatkowe informacje, zobacz ograniczenie.
Declspec noalias jest stosowany tylko do funkcji, wskazując ze funkcja jest częściowo czysta.Funkcja częściowo czystego to taka, która odwołuje się do lub modyfikuje tylko zmienne lokalne, argumenty i operatory pośrednie argumentów.Declspec jest zobowiązaniem dla kompilatora i jeśli funkcja odwołuje się do globalnych lub pośrednich operatorów drugiego poziomu argumentu wskaźnika, to kompilator może wygenerować kod, który przerywa działanie aplikacji.
Aby uzyskać dodatkowe informacje, zobacz noalias.
Optymalizacja Pragm
Istnieje również kilka przydatnych pragm, które pomagają optymalizować kod.Pierwszy zostanie omówiony #pragma optimize:
#pragma optimize("{opt-list}", on | off)
Pragma umożliwia ustawienie danego poziomu optymalizacji na podstawie kolejnych funkcji.Jest to idealne dla rzadkich przypadków, takich gdzie aplikacja zawiesza się kiedy dana funkcja jest skompilowana z optymalizacją.To umożliwia wyłączanie optymizacji dla pojedynczych funkcji:
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
Aby uzyskać dodatkowe informacje, zobacz optymalizuj.
Wbudowanie jest jednym z najważniejszych optymalizacji, które wykonuje kompilator i tutaj omówimy kilka pragm, które pomagają zmodyfikować to zachowanie.
#pragma inline_recursion przydaje się do określania, czy chcesz, aby aplikacja była zdolna do wbudowanego wywołania rekurencyjnego.Domyślnie jest wyłączona.Dla skróconych rekursji małych funkcji można włączyć tę funkcję.Aby uzyskać dodatkowe informacje, zobacz inline_recursion.
Inną przydatną pragmą do ograniczenia głębokości wbudowania jest #pragma inline_depth.Jest to zazwyczaj przydatne w sytuacjach, gdy próbujesz ograniczyć rozmiar programu lub funkcji.Aby uzyskać dodatkowe informacje, zobacz inline_depth.
__restrict i __assume
Istnieje kilka słów kluczowych w programie Visual C++, które mogą pomóc podwyższyć wydajność: __restrict i __assume.
Po pierwsze, należy zauważyć, że __restrict i __declspec(restrict) to dwie różne rzeczy.Są trochę powiązane, ale ich semantyka jest różna.__restrict jest typem kwalifikatora, tak jak const lub volatile, ale wyłącznie dla typów wskaźnikowych.
Wskaźnik, który jest modyfikowany z __restrict odnosi się jako wskaźnik __restrict.Wskaźnik __restrict jest wskaźnikiem, do którego można uzyskać dostęp tylko za pośrednictwem wskaźnika __restrict.Innymi słowy inny wskaźnik nie może uzyskać dostępu do danych wskazywanych przez wskaźnik __restrict.
__restrict może być potężnym narzędziem optymalizacji dla programu Visual C++ , ale należy używać go z dużą ostrożnością.Jeżeli jest używany w nieodpowiedni sposób, optymalizator może wykonać optymalizację, która spowoduje przerwanie działania aplikacji.
Słowo kluczowe __restrict zamienia przełącznik /Oa z poprzednich wersji.
Z __assume, deweloper może poinformować kompilator, aby zrobił założenia o wartości niektórych zmiennych.
Na przykład __assume(a < 5); informuje optymalizator że w tym wierszu kodu, zmienna a jest mniejsza niż 5.Ponownie jest to zobowiązanie dla kompilatora.Jeśli a faktycznie przyjmuje wartość 6 w programie, to zachowanie programu po optymalizacji kompilatora, może nie być takie jak oczekiwano.__assume jest najbardziej użyteczna przed instrukcjami wyrażeń warunkowych and/or.
Istnieją pewne ograniczenia w __assume.Po pierwsze, __restrict, to tylko sugestia, więc kompilator może ją ignorować.Ponadto __assume obecnie działa tylko w przypadku zmiennych nierównych wobec stałych.Nie propaguje ono symbolicznych nierówności, na przykład, załóżmy (a < b).
Wsparcie wewnętrzne
Funkcje wewnętrzne są funkcjami wywołującymi, gdzie kompilator posiada wewnętrzną wiedzę o wywołaniu i zamiast wywołać funkcję z biblioteki, emituje kod dla tej funkcji.Plik nagłówka intrin.h znajduje się w <Installation_Directory>\VC\include\intrin.h i zawiera wszystkie dostępne funkcje wewnętrzne dla każdej z trzech obsługiwanych platform (x86, x64 i ARM).
Funkcje wewnętrzne dają programiście możliwości do wejścia w głąb kodu bez konieczności używania zestawu.Istnieje kilka korzyści wynikających z używania funkcji wewnętrznych:
Kod jest bardziej przenośny.Kilka funkcji wewnętrznych jest dostępnych na wielu architekturach procesora.
Kod jest łatwiejszy do czytania, ponieważ jest napisany w języku C/C++.
Kod pobiera korzyści z optymalizacji kompilatora.Jeśli kompilator staje się lepszy, generowanie kodu dla funkcji wewnętrznych poprawia się.
Aby uzyskać więcej informacji, zobacz Funkcje wewnętrzne kompilatora i Korzyści wynikające ze stosowania Intrinsics.
Wyjątki
Jest to czynnik wpływający na wydajność a związany z używaniem wyjątków.Dla używania bloków try, które hamują kompilator przed wykonywaniem niektórych optymalizacji, wprowadzono pewne ograniczenia.Na platformach x86, występuje dodatkowe obniżenie wydajności bloków try, z powodu dodatkowej informacji o stanie, która musi zostać wygenerowana podczas wykonywania kodu.Na platformach 64-bitowych, bloki try nie zmniejszają wydajność tak bardzo, ale po wyrzuceniu wyjątku, procesy wyszukiwania programu obsługi i odwracania stosu mogą być kosztowne.
Dlatego zaleca się unikania wprowadzania bloków try/catch do kodu, który tego nie potrzebuje.Jeśli istnieje konieczność użycia wyjątków, należy użyć wyjątków synchronicznych jeżeli to możliwe.Aby uzyskać dodatkowe informacje, zobacz Obsługa wyjątków strukturalnych (C/C++).
Generują one wyjątki tylko w wyjątkowych przypadkach.Za pomocą wyjątków dla ogólnej kontroli przepływu, prawdopodobnie spowoduje, że ucierpi na tym wydajność.