Poprosiłem Google o podanie znaczenia gcc
opcji -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.
źródło
Release
iDebug
jest to bardzo przydatne, weź tę opcję jako przykład.Release
kompilację?Odpowiedzi:
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.
źródło
alloca
przesunię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.alloca
[kto robi? - Jestem na 99% pewien, że nigdy nie napisałem kodu, który używaalloca
] lubvariable 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.alloca
: są wyrzucane, gdy tylko opuścisz zakres, w którym są zadeklarowane, podczas gdyalloca
miejsce jest zwalniane tylko wtedy, gdy opuścisz funkcję. To sprawia, że VLA jest znacznie łatwiejsze do naśladowania niżalloca
, jak sądzę.-fomit-frame-pointer
domyślnie włączone dla x86-64.alloca
przestrzeń 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.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).
(ź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
enter
instrukcję, która była bardzo przydatna na procesorach Intel 80286 i 80386.Ponadto przed powrotem funkcji używany jest następujący kod:
lub
leave
instrukcja.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 frames
jest 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 lokalizowaniacall sites
na 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-pointer
lub 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:Tak więc z dokumentacji jako takiej nie wynika jasno, czy
-fomit-frame-pointer
zostanie włączony, jeśli tylko skompilujesz z jedną-O
opcją 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-pointer
mię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:
Należy pamiętać, że powyższy cytat dotyczy tylko kompilatora Intel C ++ 15, a nie GCC.
źródło
gcc -m16
istnieje, 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-pointer
jest on domyślnie włączony od lat na platformie x86-m32
i dłużej niż na platformie x86-64 (-m64
).