Próbuję zrozumieć opcję gcc -fomit-frame-pointer

80

Poprosiłem Google o podanie znaczenia gccopcji -fomit-frame-pointer, która przekierowuje mnie do poniższego oświadczenia.

-fomit-frame-pointer

Nie trzymaj wskaźnika ramki w rejestrze dla funkcji, które go nie potrzebują. Pozwala to uniknąć instrukcji zapisywania, konfigurowania i przywracania wskaźników ramek; udostępnia również dodatkowy rejestr w wielu funkcjach. Uniemożliwia również debugowanie na niektórych komputerach.

Zgodnie z moją wiedzą o każdej funkcji, rekord aktywacji zostanie utworzony na stosie pamięci procesu, aby zachować wszystkie zmienne lokalne i trochę więcej informacji. Mam nadzieję, że ten wskaźnik ramki oznacza adres rekordu aktywacji funkcji.

W takim przypadku, jaki jest typ funkcji, dla których nie musi utrzymywać wskaźnika ramki w rejestrze? Jeśli dostanę te informacje, spróbuję na tej podstawie zaprojektować nową funkcję (o ile to możliwe), ponieważ jeśli wskaźnik ramki nie jest przechowywany w rejestrach, to niektóre instrukcje zostaną pominięte w systemie binarnym. To naprawdę znacznie poprawi wydajność w aplikacji, w której jest wiele funkcji.

rashok
źródło
5
Konieczność debugowania tylko jednego zrzutu awaryjnego kodu, który został skompilowany z tą opcją, wystarczy, aby usunąć tę opcję z plików makefile. Przy okazji nie usuwa żadnych instrukcji, po prostu daje optymalizatorowi jeszcze jeden rejestr do pracy w celu przechowywania.
Hans Passant
1
@HansPassant Właściwie jest to bardzo przydatne w przypadku kompilacji wydań. Posiadanie dwóch celów w pliku Makefile - Releasei Debugjest to bardzo przydatne, weź tę opcję jako przykład.
Kotauskas
3
@VladislavToncharov Wydaje mi się, że nigdy nie musiałeś debugować zrzutu awaryjnego od klienta obsługującego twoją Releasekompilację?
Andreas Magnusson

Odpowiedzi:

60

Większość mniejszych funkcji nie potrzebuje wskaźnika ramki - większe funkcje MOGĄ go potrzebować.

Tak naprawdę chodzi o to, jak dobrze kompilatorowi udaje się śledzić, w jaki sposób jest używany stos i gdzie znajdują się rzeczy na stosie (zmienne lokalne, argumenty przekazane do bieżącej funkcji i argumenty przygotowywane dla funkcji, która ma zostać wywołana). Nie wydaje mi się, aby łatwo było scharakteryzować funkcje, które wymagają lub nie potrzebują wskaźnika ramki (technicznie rzecz biorąc, ŻADNA funkcja NIE MUSI mieć wskaźnika ramki - jest to raczej przypadek „jeśli kompilator uzna za konieczne zmniejszenie złożoności inny kod ”).

Myślę, że nie powinieneś "próbować sprawić, by funkcje nie miały wskaźnika ramki" jako część swojej strategii kodowania - tak jak powiedziałem, proste funkcje ich nie potrzebują, więc użyj -fomit-frame-pointer, a otrzymasz jeszcze jeden dostępny rejestr dla alokatora rejestrów i zapisać 1-3 instrukcje wejścia / wyjścia do funkcji. Jeśli twoja funkcja potrzebuje wskaźnika ramki, to dlatego, że kompilator zdecydował, że jest to lepsza opcja niż nieużywanie wskaźnika ramki. Nie jest celem posiadanie funkcji bez wskaźnika ramki, celem jest posiadanie kodu, który działa zarówno poprawnie, jak i szybko.

Zauważ, że "brak wskaźnika ramki" powinien dawać lepszą wydajność, ale to nie jest jakaś magiczna kula, która daje ogromne ulepszenia - szczególnie nie na x86-64, który ma już 16 rejestrów na początek. Na 32-bitowym x86, ponieważ ma tylko 8 rejestrów, z których jeden jest wskaźnikiem stosu, a zajmowanie drugiego, gdy wskaźnik ramki oznacza, że ​​zajęte jest 25% miejsca na rejestry. Zmiana tego na 12,5% to spora poprawa. Oczywiście kompilacja do wersji 64-bitowej również bardzo pomoże.

Mats Petersson
źródło
24
Zwykle kompilator może samodzielnie śledzić głębokość stosu i nie potrzebuje wskaźnika ramki. Wyjątkiem jest sytuacja, gdy funkcja używa allocaprzesunięcia wskaźnika stosu o zmienną wartość. Pominięcie wskaźnika ramki znacznie utrudnia debugowanie. Zmienne lokalne są trudniejsze do zlokalizowania, a ślady stosu są znacznie trudniejsze do zrekonstruowania bez pomocnego wskaźnika ramki. Ponadto dostęp do parametrów może być droższy, ponieważ znajdują się one daleko od szczytu stosu i mogą wymagać droższych trybów adresowania.
Raymond Chen
3
Tak, więc zakładając, że nie używamy alloca[kto robi? - Jestem na 99% pewien, że nigdy nie napisałem kodu, który używa alloca] lub variable size local arrays[co jest nowoczesną formą alloca], to kompilator MOŻE nadal zdecydować, że użycie wskaźnika ramki jest lepszą opcją - ponieważ kompilatory są napisane tak, aby nie podążały ślepo za podane opcje, ale dają najlepszy wybór.
Mats Petersson
6
@MatsPetersson VLA różnią się od alloca: są wyrzucane, gdy tylko opuścisz zakres, w którym są zadeklarowane, podczas gdy allocamiejsce jest zwalniane tylko wtedy, gdy opuścisz funkcję. To sprawia, że ​​VLA jest znacznie łatwiejsze do naśladowania niż alloca, jak sądzę.
Jens Gustedt
35
Może warto wspomnieć, że gcc ma -fomit-frame-pointerdomyślnie włączone dla x86-64.
zwol
5
@JensGustedt, problem nie polega na tym, że są wyrzucane, problem polega na tym, że ich rozmiar (podobnie jak allocaprzestrzeń ed) jest nieznany w czasie kompilacji . Zwykle kompilator użyje wskaźnika ramki, aby uzyskać adres zmiennych lokalnych, jeśli rozmiar ramki stosu się nie zmieni, może zlokalizować je w ustalonym przesunięciu względem wskaźnika stosu.
vonbrand
15

Chodzi o rejestr BP / EBP / RBP na platformach Intel. Ten rejestr domyślnie jest segmentem stosu (nie wymaga specjalnego prefiksu, aby uzyskać dostęp do segmentu stosu).

EBP to najlepszy wybór rejestru do uzyskiwania dostępu do struktur danych, zmiennych i dynamicznie przydzielanej przestrzeni roboczej w stosie. EBP jest często używany do uzyskiwania dostępu do elementów na stosie w stosunku do stałego punktu na stosie, a nie w stosunku do bieżącego TOS. Zwykle identyfikuje adres bazowy bieżącej ramki stosu ustalony dla bieżącej procedury. Gdy EBP jest używany jako rejestr bazowy w obliczaniu przesunięcia, przesunięcie jest obliczane automatycznie w bieżącym segmencie stosu (tj. Segmencie aktualnie wybranym przez SS). Ponieważ SS nie musi być jawnie określane, kodowanie instrukcji w takich przypadkach jest bardziej wydajne. EBP może być również użyty do indeksowania segmentów adresowalnych przez inne rejestry segmentów.

(źródło - http://css.csail.mit.edu/6.858/2017/readings/i386/s02_03.htm )

Ponieważ na większości platform 32-bitowych segment danych i segment stosu są takie same, to powiązanie EBP / RBP ze stosem nie stanowi już problemu. Tak jest na platformach 64-bitowych: architektura x86-64, wprowadzona przez AMD w 2003 roku, w dużej mierze porzuciła obsługę segmentacji w trybie 64-bitowym: cztery rejestry segmentów: CS, SS, DS i ES są zmuszone do zerowania Te okoliczności dla 32-bitowych i 64-bitowych platform x86 zasadniczo oznaczają, że rejestr EBP / RBP może być używany bez żadnego przedrostka w instrukcjach procesora, które mają dostęp do pamięci.

Tak więc opcja kompilatora, o której pisałeś, pozwala na użycie BP / EBP / RBP do innych celów, np. Do przechowywania zmiennej lokalnej.

Przez „To pozwala uniknąć instrukcji zapisywania, ustawiania i przywracania wskaźników ramek” oznacza unikanie następującego kodu przy wprowadzaniu każdej funkcji:

lub enterinstrukcję, która była bardzo przydatna na procesorach Intel 80286 i 80386.

Ponadto przed powrotem funkcji używany jest następujący kod:

lub leaveinstrukcja.

Narzędzia do debugowania mogą skanować dane stosu i wykorzystywać te wypchane dane rejestru EBP podczas lokalizowania call sites, tj. Wyświetlać nazwy funkcji i argumenty w kolejności, w jakiej zostały nazwane hierarchicznie.

Programiści mogą mieć pytania dotyczące ramek stosu nie w szerokim znaczeniu (że jest to pojedyncza jednostka na stosie, która obsługuje tylko jedno wywołanie funkcji i zachowuje adres zwrotny, argumenty i zmienne lokalne), ale w wąskim sensie - kiedy termin stack framesjest wspomniany w kontekst opcji kompilatora. Z punktu widzenia kompilatora ramka stosu to po prostu kodem wejścia i wyjścia dla procedury , która wypycha kotwicę do stosu - która może być również używana do debugowania i obsługi wyjątków. Narzędzia do debugowania mogą skanować dane stosu i wykorzystywać te kotwice do śledzenia wstecznego podczas lokalizowania call sitesna stosie, tj. Do wyświetlania nazw funkcji w kolejności, w jakiej zostały nazwane hierarchicznie.

Dlatego bardzo ważne jest, aby programista zrozumiał, czym jest ramka stosu pod względem opcji kompilatora - ponieważ kompilator może kontrolować, czy wygenerować ten kod, czy nie.

W niektórych przypadkach kompilator może pominąć ramkę stosu (kod wejściowy i wyjściowy procedury), a dostęp do zmiennych będzie można uzyskać bezpośrednio za pośrednictwem wskaźnika stosu (SP / ESP / RSP) zamiast wygodnego wskaźnika podstawowego (BP / ESP / RSP). Warunki dla kompilatora, aby pomijać ramki stosu dla niektórych funkcji mogą być różne, na przykład: (1) funkcja jest funkcją-liść (tj. Jednostką końcową, która nie wywołuje innych funkcji); (2) bez wyjątków; (3) żadne procedury nie są wywoływane na stosie z parametrami wychodzącymi; (4) funkcja nie ma parametrów.

Pomijanie ramek stosu (kod wejściowy i wyjściowy procedury) może sprawić, że kod będzie mniejszy i szybszy, ale może również negatywnie wpłynąć na zdolność debugerów do śledzenia wstecznego danych w stosie i wyświetlania ich programiście. Są to opcje kompilatora, które określają, w jakich warunkach funkcja powinna spełniać, aby kompilator przyznał jej kod wejścia i wyjścia ramki stosu. Na przykład kompilator może mieć opcje dodawania takiego kodu wejścia i wyjścia do funkcji w następujących przypadkach: (a) zawsze, (b) nigdy, (c) w razie potrzeby (określając warunki).

Wracając od ogólników do szczegółów: jeśli użyjesz -fomit-frame-pointer opcji kompilatora GCC, możesz wygrać zarówno na kodzie wejściowym, jak i końcowym procedury oraz na posiadaniu dodatkowego rejestru (chyba że jest już domyślnie włączony samodzielnie lub domyślnie przez inny opcje, w tym przypadku już korzystasz z korzyści wynikających z używania rejestru EBP / RBP i żadne dodatkowe korzyści nie zostaną uzyskane poprzez wyraźne określenie tej opcji, jeśli jest już włączona domyślnie). Należy jednak pamiętać, że w trybach 16-bitowych i 32-bitowych rejestr BP nie ma możliwości dostępu do 8-bitowych jego części, jak ma to AX (AL i AH).

Ponieważ ta opcja, oprócz umożliwienia kompilatorowi używania EBP jako rejestru ogólnego przeznaczenia w optymalizacjach, zapobiega również generowaniu kodu wyjścia i wejścia dla ramki stosu, co komplikuje debugowanie - dlatego dokumentacja GCC wyraźnie stwierdza (niezwykle podkreślając pogrubioną czcionką style), że włączenie tej opcji uniemożliwia debugowanie na niektórych komputerach

Należy również pamiętać, że inne opcje kompilatora, związane z debugowaniem lub optymalizacją, mogą niejawnie włączać -fomit-frame-pointerlub wyłączać tę opcję.

Nie znalazłem żadnych oficjalnych informacji na gcc.gnu.org o tym, jak inne opcje wpływają -fomit-frame-pointer na platformy x86 , https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html stwierdza tylko, co następuje:

-O włącza także -fomit-frame-wskaźnik na maszynach, na których nie koliduje to z debugowaniem.

Tak więc z dokumentacji jako takiej nie wynika jasno, czy -fomit-frame-pointerzostanie włączony, jeśli tylko skompilujesz z jedną -Oopcją na platformie x86. Można to przetestować empirycznie, ale w tym przypadku twórcy GCC nie zobowiązują się do niezmieniania zachowania tej opcji w przyszłości bez powiadomienia.

Jednak Peter Cordes zwrócił uwagę w komentarzach, że istnieje różnica w ustawieniach domyślnych -fomit-frame-pointermiędzy platformami x86-16 i x86-32 / 64.

Ta opcja - -fomit-frame-pointer- dotyczy również kompilatora Intel C ++ 15.0 , nie tylko GCC:

W przypadku kompilatora Intel ta opcja ma alias /Oy.

Oto, co o tym napisał Intel:

Te opcje określają, czy EBP jest używany jako rejestr ogólnego przeznaczenia w optymalizacjach. Opcje -fomit-frame-pointer i / Oy pozwalają na to użycie. Opcje -fno-omit-frame-pointer i / Oy- disallow.

Niektóre debuggery oczekują, że EBP będzie używany jako wskaźnik ramki stosu i nie mogą tworzyć śledzenia stosu, chyba że tak jest. Opcje -fno-omit-frame-pointer i / Oy- kierują kompilator do generowania kodu, który utrzymuje i używa EBP jako wskaźnika ramki stosu dla wszystkich funkcji, dzięki czemu debugger może nadal tworzyć ślad po stosie bez wykonywania następujących czynności:

Dla -fno-omit-frame-pointer: wyłączanie optymalizacji za pomocą -O0 For / Oy-: wyłączanie optymalizacji / O1, / O2 lub / O3 Opcja -fno-omit-frame-pointer jest ustawiana po określeniu opcji - O0 lub opcja -g. Opcja -fomit-frame-pointer jest ustawiana po określeniu opcji -O1, -O2 lub -O3.

Opcja / Oy jest ustawiana po określeniu opcji / O1, / O2 lub / O3. Opcja / Oy- jest ustawiana po określeniu opcji / Od.

Użycie opcji -fno-omit-frame-pointer lub / Oy- zmniejsza liczbę dostępnych rejestrów ogólnego przeznaczenia o 1 i może skutkować nieco mniej wydajnym kodem.

UWAGA W przypadku systemów Linux *: obecnie występuje problem z obsługą wyjątków w GCC 3.2. Dlatego kompilator Intela ignoruje tę opcję, gdy GCC 3.2 jest zainstalowany dla C ++ i obsługa wyjątków jest włączona (ustawienie domyślne).

Należy pamiętać, że powyższy cytat dotyczy tylko kompilatora Intel C ++ 15, a nie GCC.

Maxim Masiutin
źródło
1
16-bitowy kod i BP domyślnie ustawione na SS zamiast DS, nie są tak naprawdę istotne dla gcc. gcc -m16istnieje, ale jest to dziwny przypadek specjalny, który zasadniczo tworzy 32-bitowy kod, który działa w trybie 16-bitowym, używając prefiksów w każdym miejscu. Należy również pamiętać, że -fomit-frame-pointerjest on domyślnie włączony od lat na platformie x86 -m32i dłużej niż na platformie x86-64 ( -m64).
Peter Cordes,
@PeterCordes - dziękuję, zaktualizowałem zmiany zgodnie z zgłoszonymi przez Ciebie problemami.
Maxim Masiutin