Jestem początkującym w języku asemblera i zauważyłem, że kod x86 emitowany przez kompilatory zwykle utrzymuje wskaźnik ramki nawet w trybie zwolnienia / optymalizacji, kiedy mógłby użyć EBP
rejestru do czegoś innego.
Rozumiem, dlaczego wskaźnik ramki może ułatwić debugowanie kodu i może być niezbędny, jeśli alloca()
zostanie wywołany w funkcji. Jednak x86 ma bardzo mało rejestrów, a używanie dwóch z nich do przechowywania położenia ramki stosu, gdy wystarczyłoby jedno, po prostu nie ma dla mnie sensu. Dlaczego pomijanie wskaźnika ramki jest uważane za zły pomysł, nawet w kompilacjach zoptymalizowanych / wydanych?
performance
assembly
x86
dsimcha
źródło
źródło
alloca
) 3. łatwość implementacji w czasie wykonywania: obsługa wyjątków, piaskownica, GCOdpowiedzi:
Wskaźnik ramki to wskaźnik odniesienia, który pozwala debugerowi wiedzieć, gdzie znajduje się lokalna zmienna lub argument z pojedynczym stałym przesunięciem. Chociaż wartość ESP zmienia się w trakcie wykonywania, EBP pozostaje taki sam, co umożliwia osiągnięcie tej samej zmiennej przy tym samym przesunięciu (np. Pierwszy parametr zawsze będzie na poziomie EBP + 8, podczas gdy przesunięcia ESP mogą się znacznie zmienić, ponieważ będziesz naciskać) / popping rzeczy)
Dlaczego kompilatory nie odrzucają wskaźnika ramki? Ponieważ dzięki wskaźnikowi ramki debugger może dowiedzieć się, gdzie lokalne zmienne i argumenty używają tablicy symboli, ponieważ gwarantuje się, że będą one miały stałe przesunięcie względem EBP. W przeciwnym razie nie ma łatwego sposobu na określenie, gdzie w dowolnym miejscu kodu znajduje się zmienna lokalna.
Jak wspomniał Greg, pomaga to również w rozwijaniu stosu dla debuggera, ponieważ EBP zapewnia odwrotnie połączoną listę ramek stosu, pozwalając tym samym debugerowi określić rozmiar ramki stosu (zmienne lokalne + argumenty) funkcji.
Większość kompilatorów udostępnia opcję pomijania wskaźników ramek, chociaż utrudnia to debugowanie. Ta opcja nigdy nie powinna być używana globalnie, nawet w kodzie wydania. Nie wiesz, kiedy będziesz musiał debugować awarię użytkownika.
źródło
-fomit-frame-pointer
. To ustawienie jest domyślne w ostatnim gcc..eh_frame_hdr
sekcja jest również używana do obsługi wyjątków w czasie wykonywania. Znajdziesz go (zobjdump -h
) w większości plików binarnych w systemie Linux, to około 16k dla/bin/bash
, w porównaniu z 572B dla GNU/bin/true
, 108k dlaffmpeg
. Istnieje opcja gcc, aby wyłączyć jego generowanie, ale jest to „normalna” sekcja danych, a nie sekcja debugowania, która jeststrip
usuwana domyślnie. W przeciwnym razie nie można by przeszukać funkcji bibliotecznej, która nie miała symboli debugowania. Ta sekcja może być większa niżpush/mov/pop
instrukcje, które zastępuje, ale ma prawie zerowy koszt wykonania (np. Pamięć podręczna uop).Dodam tylko moje dwa grosze do już dobrych odpowiedzi.
Łańcuch ramek stosu jest częścią dobrej architektury języka. BP wskazuje na bieżącą ramkę, w której przechowywane są zmienne lokalne podprogramu. (Lokalni mieszkańcy mają ujemne przesunięcia, a argumenty mają dodatnie przesunięcia).
Pomysł, że uniemożliwia to wykorzystanie idealnie dobrego rejestru w optymalizacji, rodzi pytanie: kiedy i gdzie optymalizacja naprawdę się opłaca?
Optymalizacja jest opłacalna tylko w ciasnych pętlach, które 1) nie wywołują funkcji, 2) w których licznik programu spędza znaczną część swojego czasu oraz 3) w kodzie, który kompilator kiedykolwiek zobaczy (tj. Funkcje niebędące bibliotekami). Zwykle jest to bardzo mała część całego kodu, szczególnie w dużych systemach.
Inny kod można przekręcić i ścisnąć, aby pozbyć się cykli, a to po prostu nie ma znaczenia, ponieważ licznika programu praktycznie nigdy nie ma.
Wiem, że o to nie pytałeś, ale z mojego doświadczenia wynika, że 99% problemów z wydajnością nie ma nic wspólnego z optymalizacją kompilatora. Mają wszystko wspólnego z przeprojektowaniem.
źródło
Z pewnością zależy to od kompilatora. Widziałem zoptymalizowany kod emitowany przez kompilatory x86, które swobodnie używają rejestru EBP jako rejestru ogólnego przeznaczenia. (Nie pamiętam jednak, z którym kompilatorem to zauważyłem).
Kompilatory mogą również zdecydować się na utrzymanie rejestru EBP, aby pomóc w rozwijaniu stosu podczas obsługi wyjątków, ale znowu zależy to od dokładnej implementacji kompilatora.
źródło
-fomit-frame-pointer
włącza optymalizację. (gdy ABI na to pozwala). GCC, clang, ICC i MSVC robią to, IIRC, nawet w przypadku 32-bitowego systemu Windows. Tak, moja odpowiedź na pytanie: Dlaczego lepiej jest używać ebp niż rejestru esp do lokalizowania parametrów na stosie? pokazuje, że nawet 32-bitowy system Windows może pomijać wskaźnik ramki. 32-bitowy Linux x86 zdecydowanie potrafi i tak. I oczywiście 64-bitowe ABI pozwoliły na pomijanie wskaźnika ramki od samego początku.Jest to prawdą tylko w tym sensie, że kody operacyjne mogą adresować tylko 8 rejestrów. Sam procesor w rzeczywistości będzie miał o wiele więcej rejestrów niż to i użyje zmiany nazwy rejestrów, potokowania, wykonywania spekulatywnego i innych modnych słów procesora, aby obejść ten limit. Wikipedia ma dobry akapit wprowadzający na temat tego, co procesor x86 może zrobić, aby pokonać limit rejestrów: http://en.wikipedia.org/wiki/X86#Current_implementations .
źródło
Korzystanie z ramek stosowych stało się niewiarygodnie tanie w każdym sprzęcie, nawet zdalnie nowoczesnym. Jeśli masz tanie ramki stosu, zapisanie kilku rejestrów nie jest tak ważne. Jestem pewien, że ramki szybkiego stosu w porównaniu z większą liczbą rejestrów były kompromisem inżynieryjnym, a ramki szybkiego stosu wygrały.
Ile oszczędzasz przechodząc na czysty rejestr? Czy warto?
źródło