Czy poziom optymalizacji -O3 jest niebezpieczny w g ++?

232

Słyszałem z różnych źródeł (choć głównie od mojego kolegi), że kompilacja z poziomem optymalizacji -O3g ++ jest w jakiś sposób „niebezpieczna” i ogólnie należy jej unikać, chyba że okaże się to konieczne.

Czy to prawda, a jeśli tak, to dlaczego? Czy powinienem się trzymać -O2?

Dunnie
źródło
38
Jest to niebezpieczne tylko wtedy, gdy polegasz na niezdefiniowanym zachowaniu. I nawet wtedy byłbym zaskoczony, gdyby to poziom optymalizacji coś zepsuł.
Seth Carnegie
5
Kompilator jest nadal ograniczony do tworzenia programu, który zachowuje się „tak, jakby” dokładnie skompilował kod. Nie wiem, czy -O3jest to uważane za szczególnie wadliwe? Myślę, że może może pogorszyć nieokreślone zachowanie, ponieważ może robić dziwne i cudowne rzeczy w oparciu o pewne założenia, ale to byłaby twoja wina. Więc ogólnie powiedziałbym, że jest w porządku.
BoBTFish,
5
To prawda, że ​​wyższe poziomy optymalizacji są bardziej podatne na błędy kompilatora. Sam trafiłem na kilka przypadków, ale ogólnie są one wciąż dość rzadkie.
Mysticial
21
-O2włącza się -fstrict-aliasing, a jeśli Twój kod przetrwa, prawdopodobnie przetrwa inne optymalizacje, ponieważ ludzie często się mylą. To powiedziawszy, -fpredictive-commoningjest tylko włączone -O3, a włączenie to może umożliwić błędy w twoim kodzie spowodowane nieprawidłowymi założeniami dotyczącymi współbieżności. Im mniej błędny jest twój kod, tym mniej niebezpieczna jest optymalizacja ;-)
Steve Jessop
6
@PlasmaHH, nie sądzę, aby „bardziej rygorystyczny” był dobrym opisem -Ofast, wyłącza na przykład obsługę NaNs zgodną z IEEE
Jonathan Wakely

Odpowiedzi:

223

We wczesnych dniach gcc (2.8 itd.) Oraz w czasach egcs i redhat 2.96-O3 bywał czasem dość błędny. Ale to już ponad dziesięć lat temu, a -O3 niewiele różni się od innych poziomów optymalizacji (w buggyness).

Zwykle jednak ujawnia przypadki, w których ludzie polegają na nieokreślonym zachowaniu, z powodu bardziej ścisłego polegania na regułach, a zwłaszcza narożnych przypadkach języka (języków).

Osobiście, od wielu lat prowadzę oprogramowanie produkcyjne w sektorze finansowym z opcją -O3 i nie spotkałem się jeszcze z błędem, którego nie byłoby, gdybym użył opcji -O2.

Według popularnego popytu, tutaj dodatek:

-O3, a zwłaszcza dodatkowe flagi, takie jak -funroll-loop (nie włączone przez -O3) mogą czasami prowadzić do generowania większej liczby kodów maszynowych. W pewnych okolicznościach (np. Na jednostce centralnej z wyjątkowo małą pamięcią podręczną instrukcji L1) może to spowodować spowolnienie z powodu całego kodu np. Jakiejś wewnętrznej pętli, która nie pasuje już do L1I. Zasadniczo gcc bardzo mocno stara się nie generować tak dużej ilości kodu, ale ponieważ zazwyczaj optymalizuje ogólny przypadek, może się to zdarzyć. Opcje szczególnie na to podatne (jak rozwijanie pętli) zwykle nie są zawarte w -O3 i są odpowiednio oznaczone na stronie podręcznika. W związku z tym ogólnie dobrym pomysłem jest użycie opcji -O3 do generowania szybkiego kodu i polegać tylko na opcji -O2 lub -Os (która próbuje zoptymalizować rozmiar kodu), gdy jest to właściwe (np. Gdy profiler wskazuje brak L1I).

Jeśli chcesz maksymalnie wykorzystać optymalizację, możesz dostosować gcc za pomocą --param kosztów związanych z niektórymi optymalizacjami. Dodatkowo zauważ, że gcc ma teraz możliwość umieszczania atrybutów w funkcjach, które kontrolują ustawienia optymalizacji tylko dla tych funkcji, więc gdy okaże się, że masz problem z -O3 w jednej funkcji (lub chcesz wypróbować specjalne flagi dla tej funkcji), nie musisz kompilować całego pliku ani nawet całego projektu za pomocą O2.

otoh wydaje się, że należy zachować ostrożność podczas używania opcji -Ofast, która stwierdza:

-Ofast włącza wszystkie optymalizacje -O3. Umożliwia także optymalizacje, które nie są prawidłowe dla wszystkich standardowych programów zgodnych.

co prowadzi mnie do wniosku, że -O3 ma być w pełni zgodny ze standardami.

Plazma
źródło
2
Po prostu używam czegoś przeciwnego. Zawsze używam -Os lub -O2 (czasami O2 generuje mniejszy plik wykonywalny) .. po profilowaniu używam O3 na częściach kodu, które wymagają więcej czasu wykonania i tylko to może dać do 20% większą prędkość.
CoffeDeveloper
3
Robię to dla szybkości. O3 najczęściej spowalnia pracę. Nie wiem dokładnie dlaczego, podejrzewam, że to zanieczyszcza pamięć podręczną instrukcji.
CoffeDeveloper
4
@DarioOO Mam ochotę powoływać się na „rozdęcie kodu” jest popularną rzeczą, ale prawie nigdy nie widzę poparcia dla testów porównawczych. Wiele zależy od architektury, ale za każdym razem, gdy widzę opublikowane testy porównawcze (np Phoronix.com/… ), pokazuje, że O3 jest szybsze w zdecydowanej większości przypadków. Widziałem profilowanie i staranną analizę wymaganą, aby udowodnić, że rozdęcie kodu było w rzeczywistości problemem i zwykle dzieje się tak tylko w przypadku osób, które przyjmują szablony w skrajny sposób.
Nir Friedman
1
@NirFriedman: Problem pojawia się, gdy w modelu kompilacji opartym na koszcie inklinacyjnym występują błędy lub gdy optymalizujesz się pod kątem zupełnie innego celu niż ten, na którym działasz. Co ciekawe, dotyczy to wszystkich poziomów optymalizacji ...
PlasmaHH
1
@PlasmaHH: problem przy użyciu cmov byłby trudny do rozwiązania w ogólnym przypadku. Zwykle nie tylko sortowane dane, więc kiedy gcc stara się zdecydować, czy oddział jest przewidywalny, czy nie, analiza statyczna szuka wywołań std::sortfunkcji jest mało prawdopodobne, aby pomoc. Użycie czegoś takiego jak stackoverflow.com/questions/109710/... pomogłoby lub może napisać źródło, aby skorzystać z posortowanej: skanuj, aż zobaczysz> = 128, a następnie zacznij sumowanie. Jeśli chodzi o rozdęty kod, tak, zamierzam przejść do zgłaszania go. : P
Peter Cordes
42

Z mojego nieco szachowego doświadczenia, zastosowanie -O3do całego programu prawie zawsze powoduje spowolnienie (w stosunku do -O2), ponieważ włącza agresywne rozwijanie i wstawianie pętli, które powoduje, że program nie mieści się już w pamięci podręcznej instrukcji. W przypadku większych programów może to być również prawdą w -O2odniesieniu do -Os!

Zamierzonym sposobem użycia -O3jest, po profilowaniu programu, ręczne zastosowanie go do niewielkiej garści plików zawierających krytyczne pętle wewnętrzne, które faktycznie korzystają z tych agresywnych kompromisów między szybkością a szybkością. Nowsze wersje GCC mają tryb optymalizacji sterowany profilem, który może (IIUC) selektywnie stosować -O3optymalizacje do gorących funkcji - skutecznie automatyzując ten proces.

zwol
źródło
10
"prawie zawsze"? Zrób to „50-50”, a my będziemy mieli ofertę ;-).
No-Bugs Hare
12

Opcja -O3 włącza droższe optymalizacje, takie jak wstawianie funkcji, oprócz wszystkich optymalizacji niższych poziomów „-O2” i „-O1”. Poziom optymalizacji „-O3” może zwiększyć szybkość wynikowego pliku wykonywalnego, ale może również zwiększyć jego rozmiar. W pewnych okolicznościach, gdy te optymalizacje nie są korzystne, ta opcja może faktycznie spowolnić program.

Neel
źródło
3
Rozumiem, że niektóre „pozorne optymalizacje” mogą spowolnić program, ale czy masz źródło, które twierdzi, że GCC-O3 spowolnił program?
Mooing Duck
1
@MooingDuck: Chociaż nie mogę zacytować źródła, pamiętam, że wpadłem na taką przypadek z niektórymi starszymi procesorami AMD, które miały dość małą pamięć podręczną L1I (~ 10 000 instrukcji). Jestem pewien, że Google ma więcej dla zainteresowanych, ale zwłaszcza opcje takie jak rozwijanie pętli nie są częścią O3, a te znacznie zwiększają rozmiary. -Os jest tym, gdy chcesz, aby plik wykonywalny był najmniejszy. Nawet -O2 może zwiększyć rozmiar kodu. Ładnym narzędziem do gry z wynikami różnych poziomów optymalizacji jest gcc explorer.
PlasmaHH
@PlasmaHH: W rzeczywistości niewielki rozmiar pamięci podręcznej jest czymś, co kompilator mógłby spieprzyć, dobra uwaga. To naprawdę dobry przykład. Proszę podać to w odpowiedzi.
Mooing Duck
1
@PlasmaHH Pentium III miał pamięć podręczną 16 KB. AMD K6 i nowsze miały w rzeczywistości pamięć podręczną 32 KB. P4 zaczęło od wartości około 96 KB. Rdzeń I7 faktycznie ma pamięć podręczną kodu L1 o pojemności 32 KB. Dekodery instrukcji są obecnie mocne, więc Twój L3 jest wystarczająco dobry, aby polegać na prawie każdej pętli.
doug65536
1
Za każdym razem, gdy wywoływana jest funkcja w pętli, zobaczysz ogromny wzrost wydajności, który może znacznie wyeliminować podwyrażenie i doprowadzić do niepotrzebnego ponownego obliczenia funkcji przed pętlą.
doug65536
8

Tak, O3 jest błędny. Jestem programistą kompilatorów i podczas tworzenia własnego oprogramowania zidentyfikowałem wyraźne i oczywiste błędy gcc spowodowane przez generowanie przez O3 błędnych instrukcji montażu SIMD. Z tego, co widziałem, większość oprogramowania produkcyjnego jest dostarczana z O2, co oznacza, że ​​O3 otrzyma mniej uwagi na testowanie i naprawianie błędów.

Pomyśl o tym w ten sposób: O3 dodaje więcej transformacji na O2, co dodaje więcej transformacji na O1. Statystycznie rzecz biorąc, więcej transformacji oznacza więcej błędów. Dotyczy to każdego kompilatora.

David Yeager
źródło
3

Ostatnio wystąpił problem z korzystaniem z optymalizacji z g++. Problem dotyczył karty PCI, w której rejestry (dla poleceń i danych) były reprezentowane przez adres pamięci. Mój sterownik zamapował adres fizyczny na wskaźnik w aplikacji i podał go wywoływanemu procesowi, który działał z nim w następujący sposób:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

Karta nie działała zgodnie z oczekiwaniami. Kiedy zobaczyłem zespół zrozumiałem, że kompilator napisał tylko someCommand[ the last ]dopciMemory , pomijając wszystkie poprzednie zapisy.

Podsumowując: bądź dokładny i uważny dzięki optymalizacji.

borisbn
źródło
38
Ale chodzi o to, że twój program po prostu ma niezdefiniowane zachowanie; Optymalizator nie zrobił nic złego. W szczególności musisz zadeklarować pciMemoryjako volatile.
Konrad Rudolph,
11
W rzeczywistości nie jest to UB, ale kompilator ma prawo pominąć wszystkie oprócz ostatnich zapisów, pciMemoryponieważ wszystkie inne zapisy prawdopodobnie nie mają żadnego efektu. Optymalizator jest niesamowity, ponieważ może usunąć wiele bezużytecznych i czasochłonnych instrukcji.
Konrad Rudolph,
4
Znalazłem to w standardzie (po ponad 10 latach)) - Deklaracja ulotna może być użyta do opisania obiektu odpowiadającego odwzorowanemu w pamięci portowi wejścia / wyjścia lub obiektowi, do którego dostęp uzyskuje funkcja asynchronicznie przerywająca. Zadania na obiektach zadeklarowanych w ten sposób nie będą „optymalizowane” przez implementację lub ponownie uporządkowane, z wyjątkiem przypadków dozwolonych przez reguły oceny wyrażeń.
borisbn
2
@borisbn Trochę nie na temat, ale skąd wiesz, że Twoje urządzenie podjęło polecenie przed wysłaniem nowego polecenia?
user877329,
3
@ user877329 Widziałem to po zachowaniu urządzenia, ale to była świetna
wyprawa