Problemy specyficzne dla języka C ++
Przede wszystkim nie istnieje tak zwany przydział „stosu” lub „stosu”, który jest wymagany przez C ++ . Jeśli mówisz o automatycznych obiektach w zakresach bloków, nie są one nawet „przydzielane”. (BTW, automatyczny czas przechowywania w C zdecydowanie NIE jest taki sam jak „przydzielony”; ten ostatni jest „dynamiczny” w języku C ++.) Dynamicznie przydzielana pamięć znajduje się w wolnym magazynie , niekoniecznie w „stercie”, chociaż ta ostatnia jest często (domyślną) implementacją .
Chociaż zgodnie z regułami semantycznymi abstrakcyjnych maszyn , obiekty automatyczne nadal zajmują pamięć, zgodna implementacja C ++ może zignorować ten fakt, gdy może udowodnić, że to nie ma znaczenia (gdy nie zmienia obserwowalnego zachowania programu). To zezwolenie jest udzielane przez regułę „jak gdyby” w ISO C ++, która jest również ogólną klauzulą umożliwiającą zwykłe optymalizacje (aw ISO C istnieje prawie taka sama reguła). Oprócz zasady „tak, jak”, ISO C ++ ma również reguły wymuszania kopiiaby umożliwić pominięcie określonych dzieł obiektów. W ten sposób omawiane wywołania konstruktora i destruktora są pomijane. W rezultacie obiekty automatyczne (jeśli istnieją) w tych konstruktorach i destruktorach są również eliminowane w porównaniu z naiwną abstrakcyjną semantyką sugerowaną przez kod źródłowy.
Z drugiej strony, bezpłatna alokacja sklepu jest zdecydowanie „alokacją” z założenia. Zgodnie z regułami ISO C ++ taki przydział może zostać osiągnięty przez wywołanie funkcji przydziału . Jednak od ISO C ++ 14 wprowadzono nową zasadę („nie jak gdyby”), która zezwala na łączenie ::operator new
wywołań funkcji globalnej alokacji (tj. ) W określonych przypadkach. Tak więc części operacji alokacji dynamicznej mogą być również niedostępne, jak w przypadku obiektów automatycznych.
Funkcje alokacji przydzielają zasoby pamięci. Obiekty mogą być dalej alokowane na podstawie alokacji przy użyciu alokatorów. W przypadku obiektów automatycznych są one prezentowane bezpośrednio - chociaż dostęp do pamięci podstawowej można uzyskać i wykorzystać do zapewnienia pamięci innym obiektom (poprzez umieszczenie new
), ale nie ma to większego sensu jako bezpłatny sklep, ponieważ nie ma możliwości przeniesienia zasoby gdzie indziej.
Wszystkie inne obawy są poza zakresem C ++. Niemniej jednak mogą być nadal znaczące.
O implementacjach C ++
C ++ nie ujawnia zreifikowanych rekordów aktywacyjnych ani niektórych pierwszorzędnych kontynuacji (np. Przez słynnych call/cc
), nie ma możliwości bezpośredniego manipulowania ramkami rekordów aktywacyjnych - w których implementacja musi umieścić automatyczne obiekty. Gdy nie ma (nieprzenośnych) interoperacyjności z podstawową implementacją („natywny” nieprzenośny kod, taki jak wbudowany kod zestawu), pominięcie podstawowej alokacji ramek może być dość trywialne. Na przykład, gdy wywoływana funkcja jest wstawiana, ramki mogą być skutecznie łączone w inne, więc nie ma sposobu, aby pokazać, co to jest „przydział”.
Jednak po respekcie interakcje stają się skomplikowane. Typowa implementacja C ++ ujawni zdolność interopu na ISA (architektura zestawu instrukcji) z pewnymi konwencjami wywoływania jako granicy binarnej współdzielonej z natywnym (maszynowym na poziomie ISA) kodem. Byłoby to wyraźnie kosztowne, zwłaszcza w przypadku utrzymywania wskaźnika stosu , który często jest bezpośrednio przechowywany przez rejestr na poziomie ISA (z zapewnieniem dostępu do konkretnych instrukcji maszyny). Wskaźnik stosu wskazuje granicę górnej ramki wywołania funkcji (aktualnie aktywnego). Po wprowadzeniu wywołania funkcji potrzebna jest nowa ramka, a wskaźnik stosu jest dodawany lub odejmowany (w zależności od konwencji ISA) o wartość nie mniejszą niż wymagany rozmiar ramki. Ramka jest następnie powiedziana alokowanagdy wskaźnik stosu po operacjach. Parametry funkcji mogą być również przekazywane na ramkę stosu, w zależności od przyjętej konwencji wywołania. Ramka może przechowywać pamięć automatycznych obiektów (prawdopodobnie łącznie z parametrami) określonych przez kod źródłowy C ++. W sensie takich implementacji obiekty te są „przydzielane”. Kiedy sterowanie wychodzi z wywołania funkcji, ramka nie jest już potrzebna, zwykle jest zwalniana przez przywrócenie wskaźnika stosu z powrotem do stanu przed wywołaniem (zapisanego wcześniej zgodnie z konwencją wywoływania). Można to uznać za „zwolnienie”. Operacje te sprawiają, że rekord aktywacji skutecznie stanowi strukturę danych LIFO, dlatego często nazywany jest „ stosem (wywołania) ”.
Ponieważ większość implementacji C ++ (szczególnie tych ukierunkowanych na natywny kod na poziomie ISA i wykorzystujących język asemblera jako jego natychmiastowe wyjście) używa podobnych strategii takich jak ta, taki mylący schemat „alokacji” jest popularny. Takie alokacje (jak również dezalokacje) zużywają cykle maszynowe i może być kosztowne, gdy często pojawiają się (niezoptymalizowane) wywołania, nawet jeśli współczesne mikroarchitekty procesora mogą mieć skomplikowane optymalizacje implementowane przez sprzęt dla wspólnego wzorca kodu (np. Przy użyciu stos silnika w implementacji PUSH
/ POP
instrukcjach).
Ale tak czy inaczej, ogólnie prawdą jest, że koszt przydziału ramki stosu jest znacznie mniejszy niż wywołanie funkcji alokacji obsługującej darmowy magazyn (chyba że jest całkowicie zoptymalizowany) , który sam może mieć setki (jeśli nie miliony :-) operacji w celu utrzymania wskaźnika stosu i innych stanów. Funkcje alokacji są zazwyczaj oparte na interfejsie API udostępnianym przez środowisko hostowane (np. Środowisko wykonawcze dostarczane przez system operacyjny). W odróżnieniu od celu przechowywania automatycznych obiektów dla wywołań funkcji, takie alokacje mają charakter ogólny, więc nie będą miały struktury ramek jak stos. Tradycyjnie przydzielają miejsce z pamięci puli zwanej stertą (lub kilkoma stertami). W odróżnieniu od „stosu” pojęcie „sterty” nie wskazuje tutaj na używaną strukturę danych; która pochodzi z wczesnych implementacji języka sprzed dziesięcioleci. (BTW, stos wywołań jest zwykle przydzielany przez środowisko ze stałą lub określoną przez użytkownika wielkością ze stosu podczas uruchamiania programu lub wątku.) Charakter przypadków użycia sprawia, że przydzielanie i zwalnianie ze stosu jest znacznie bardziej skomplikowane (niż wypychanie lub pop stosy ramek) i trudno jest je bezpośrednio zoptymalizować sprzętowo.
Wpływ na dostęp do pamięci
Zwykły przydział stosu zawsze umieszcza nową ramkę na górze, więc ma całkiem dobrą lokalizację. Jest to przyjazne dla pamięci podręcznej. OTOH, pamięć przydzielana losowo w bezpłatnym sklepie nie ma takiej właściwości. Od ISO C ++ 17 istnieją szablony zasobów puli dostarczane przez <memory>
. Bezpośrednim celem takiego interfejsu jest umożliwienie, aby wyniki kolejnych alokacji były blisko siebie w pamięci. Potwierdza to fakt, że strategia ta jest ogólnie dobra pod względem wydajności we współczesnych implementacjach, np. Jest przyjazna dla buforowania w nowoczesnych architekturach. Chodzi jednak o wydajność dostępu, a nie o alokację .
Konkurencja
Oczekiwanie na równoczesny dostęp do pamięci może mieć różny wpływ na stos i stosy. Stos wywołań jest zwykle własnością jednego wątku wykonania w implementacji C ++. OTOH, stosy są często dzielone między wątkami w procesie. W przypadku takich hałd funkcje alokacji i dezalokacji muszą chronić wspólną wewnętrzną strukturę danych administracyjnych przed wyścigiem danych. W rezultacie przydziały i zwolnienia sterty mogą mieć dodatkowy narzut z powodu wewnętrznych operacji synchronizacji.
Wydajność przestrzeni
Ze względu na naturę przypadków użycia i wewnętrznych struktur danych, stosy mogą cierpieć z powodu fragmentacji pamięci wewnętrznej , podczas gdy stos nie. Nie ma to bezpośredniego wpływu na wydajność alokacji pamięci, ale w systemie z pamięcią wirtualną niska efektywność miejsca może pogorszyć ogólną wydajność dostępu do pamięci. Jest to szczególnie okropne, gdy dysk twardy jest używany jako miejsce wymiany pamięci fizycznej. Może to powodować dość długie opóźnienia - czasami miliardy cykli.
Ograniczenia przydziału stosu
Chociaż przydziały stosu są często lepsze w porównaniu z przydziałami stosu, w rzeczywistości nie oznacza to, że przydziały stosu zawsze mogą zastąpić przydziały stosu.
Po pierwsze, nie ma możliwości przydzielenia miejsca na stosie o rozmiarze określonym w środowisku wykonawczym w przenośny sposób z ISO C ++. Istnieją rozszerzenia zapewniane przez implementacje takie jak alloca
VLA (tablica o zmiennej długości), ale istnieją powody, aby ich unikać. (IIRC, źródło Linuxa ostatnio usuwa korzystanie z VLA.) (Należy również pamiętać, że ISO C99 ma obowiązkowe VLA, ale ISO C11 włącza obsługę opcjonalną.)
Po drugie, nie ma niezawodnego i przenośnego sposobu na wykrycie wyczerpania przestrzeni stosu. Jest to często nazywane przepełnieniem stosu (hmm, etymologia tej witryny) , ale prawdopodobnie bardziej dokładnie, przepełnienie stosu . W rzeczywistości często powoduje to nieprawidłowy dostęp do pamięci, a stan programu jest wówczas uszkodzony (... lub, co gorsza, dziura w zabezpieczeniach). W rzeczywistości ISO C ++ nie ma pojęcia „stos” i sprawia, że zachowanie jest niezdefiniowane, gdy zasób jest wyczerpany . Zachowaj ostrożność, ile miejsca powinno pozostać dla automatycznych obiektów.
Jeśli skończy się miejsce na stosie, na stosie jest przydzielonych zbyt wiele obiektów, co może być spowodowane zbyt dużą liczbą aktywnych wywołań funkcji lub niewłaściwym użyciem automatycznych obiektów. Takie przypadki mogą sugerować istnienie błędów, np. Wywołanie funkcji rekurencyjnej bez poprawnych warunków wyjścia.
Niemniej jednak czasami pożądane są głębokie połączenia rekurencyjne. W implementacjach języków wymagających obsługi niezwiązanych aktywnych połączeń (gdzie głębokość połączeń jest ograniczona tylko przez całkowitą pamięć), niemożliwe jest użycie (współczesnego) stosu wywołań bezpośrednio jako rekordu aktywacji języka docelowego, jak typowe implementacje C ++. Aby obejść ten problem, potrzebne są alternatywne sposoby budowy rekordów aktywacyjnych. Na przykład SML / NJ jawnie przydziela ramki na stercie i używa stosów kaktusów . Skomplikowany przydział takich ramek rekordów aktywacyjnych zwykle nie jest tak szybki jak ramek stosu wywołań. Jeśli jednak takie języki zostaną wdrożone dalej z gwarancją właściwej rekurencji ogona, bezpośredni przydział stosu w języku obiektowym (to znaczy „obiekt” w tym języku nie jest przechowywany jako referencje, ale rodzime prymitywne wartości, które mogą być odwzorowane jeden na jeden na nieudostępnionych obiektach C ++) jest jeszcze bardziej skomplikowane z większą liczbą kara za wyniki ogólnie. Podczas używania C ++ do implementacji takich języków trudno jest oszacować wpływ na wydajność.