Dlaczego książki .Net mówią o alokacji pamięci stosu a sterty?

36

Wygląda na to, że każda książka .net mówi o typach wartości vs. typach referencyjnych i wskazuje na (często niepoprawnie) stan, w którym każdy typ jest przechowywany - sterty lub stosu. Zwykle znajduje się w kilku pierwszych rozdziałach i jest przedstawiany jako bardzo ważny fakt. Myślę, że jest nawet objęty egzaminami certyfikacyjnymi . Dlaczego stos zamiast sterty ma nawet znaczenie dla (początkujących) programistów .Net? Przydzielasz rzeczy i to po prostu działa, prawda?

Greg
źródło
11
Niektórzy autorzy po prostu źle oceniają, co jest ważne, aby uczyć początkujących, a co bez znaczenia. W książce, którą ostatnio widziałem, pierwsza wzmianka o modyfikatorach dostępu zawierała już chronione wewnętrzne , których nigdy nie używałem od 6 lat C # ...
Timwi
1
Domyślam się, że ktokolwiek napisał oryginalną dokumentację .Net dla tej części, zrobił to w dużej mierze, i na tej dokumentacji autorzy oparli swoje książki, a potem po prostu pozostali.
Greg
Powiedzenie, że typy wartości kopiują całość, a odwołania nie, będą miały większy sens i łatwiej będzie zrozumieć, dlaczego należy używać odwołań, ponieważ miejsca przechowywania tych wartości mogą być specyficzne dla implementacji, a nawet nieistotne.
Trynidad
Wywiad kult kultu?
Den

Odpowiedzi:

37

Przekonałem się, że głównym powodem, dla którego ta część informacji jest uważana za ważną, jest tradycja. W środowiskach niezarządzanych rozróżnienie między stosem a stertą jest ważne i musimy ręcznie przydzielić i usunąć używaną pamięć. Teraz zarządzanie śmieciami zajmuje się zarządzaniem, więc ignorują ten fragment. Nie sądzę, że wiadomość naprawdę się przeszła, że ​​nie musimy się przejmować, jakiego rodzaju pamięci używa się.

Jak zauważył Fede, Eric Lippert ma kilka bardzo ciekawych rzeczy do powiedzenia na ten temat: http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx .

W świetle tych informacji możesz dostosować mój pierwszy akapit, aby w zasadzie czytać: „Powodem, dla którego ludzie umieszczają te informacje i zakładają, że są one ważne, jest to, że są to nieprawidłowe lub niekompletne informacje w połączeniu z potrzebą tej wiedzy w przeszłości”.

Dla tych, którzy uważają, że jest to nadal ważne ze względu na wydajność: Jakie działania wykonałbyś, aby przenieść coś ze stosu na stos, gdybyś dokonał pomiaru i stwierdził, że to miało znaczenie? Bardziej prawdopodobne jest, że znajdziesz zupełnie inny sposób na poprawę wydajności w obszarze problemu.

John Fisher
źródło
6
Słyszałem, że w niektórych implementacjach frameworka (szczególnie kompaktowych na Xbox) lepiej jest używać struktur podczas renderowania (sama gra), aby ograniczyć zbieranie śmieci. Nadal używałbyś normalnych typów gdzie indziej, ale wstępnie przydzielonych, aby GC nie działał podczas gry. To jedyna optymalizacja dotycząca stosu w porównaniu ze stosem, jaką znam w .NET, i jest dość specyficzna dla potrzeb kompaktowego frameworka i programów w czasie rzeczywistym.
CodexArcanum
5
W większości zgadzam się z argumentem tradycji. Wielu doświadczonych programistów w pewnym momencie mogło programować w języku niskiego poziomu, w którym te rzeczy miały znaczenie, jeśli chciałeś poprawnego i wydajnego kodu. Weźmy jednak na przykład C ++, język niezarządzany: oficjalna specyfikacja nie mówi, że zmienne automatyczne muszą iść na stos itp. Standard C ++ traktuje stos i stos jako szczegóły implementacji. +1
stakx
36

Wygląda na to, że każda książka .NET mówi o typach wartości vs typach referencyjnych i wskazuje na (często niepoprawnie) stan, w którym każdy typ jest przechowywany - sterty lub stosu. Zwykle znajduje się w kilku pierwszych rozdziałach i jest przedstawiany jako bardzo ważny fakt.

Zgadzam się całkowicie; Cały czas to widzę.

Dlaczego książki .NET mówią o alokacji pamięci stosu a sterty?

Jednym z powodów jest to, że wiele osób przyszło do C # (lub innych języków .NET) z tła C lub C ++. Ponieważ te języki nie egzekwują reguł dotyczących okresu przechowywania, musisz znać te reguły i dokładnie wdrożyć program, aby ich przestrzegać.

Teraz znajomość tych reguł i przestrzeganie ich w C nie wymaga zrozumienia „sterty” i „stosu”. Ale jeśli rozumiesz, jak działają struktury danych, często łatwiej jest zrozumieć i przestrzegać zasad.

Pisząc książkę dla początkujących, naturalne jest, że autor wyjaśnia pojęcia w tej samej kolejności, w jakiej się ich nauczył. To niekoniecznie kolejność, która ma sens dla użytkownika. Niedawno byłem redaktorem ds. Technicznych książki dla początkujących C # 4 Scotta Dormana, a jedną z rzeczy, które mi się podobały, było to, że Scott wybrał dość rozsądną kolejność tematów, zamiast zaczynać od naprawdę zaawansowanych tematów w zarządzaniu pamięcią.

Innym powodem jest to, że niektóre strony w dokumentacji MSDN silnie podkreślają kwestie związane z pamięcią masową. Szczególnie starsza dokumentacja MSDN, która wciąż kręci się od pierwszych dni. Znaczna część tej dokumentacji zawiera subtelne błędy, które nigdy nie zostały usunięte, i musisz pamiętać, że została napisana w określonym czasie w historii i dla określonej grupy odbiorców.

Dlaczego stos zamiast sterty ma nawet znaczenie dla (początkujących) programistów .NET?

Moim zdaniem tak nie jest. O wiele ważniejsze jest zrozumienie takich rzeczy jak:

  • Jaka jest różnica w semantyce kopiowania między typem odniesienia a typem wartości?
  • Jak zachowuje się parametr „ref int x”?
  • Dlaczego typy wartości powinny być niezmienne?

I tak dalej.

Przydzielasz rzeczy i to po prostu działa, prawda?

To jest idealne.

Są sytuacje, w których ma to znaczenie. Odśmiecanie jest niesamowite i stosunkowo niedrogie, ale nie jest bezpłatne. Kopiowanie małych struktur jest stosunkowo niedrogie, ale nie jest darmowe. Istnieją realistyczne scenariusze wydajności, w których należy zrównoważyć koszty presji zbierania i koszty nadmiernego kopiowania. W takich przypadkach bardzo pomocne jest dokładne zrozumienie wielkości, lokalizacji i rzeczywistego czasu życia całej odpowiedniej pamięci.

Podobnie istnieją realistyczne scenariusze interakcji, w których trzeba wiedzieć, co znajduje się na stosie, a co na stercie i co może przemieszczać się śmieciarz. Właśnie dlatego C # ma funkcje takie jak „naprawiono”, „stackalloc” i tak dalej.

Ale to są wszystkie zaawansowane scenariusze. Idealnie początkujący programista nie musi się martwić o nic z tych rzeczy.

Eric Lippert
źródło
2
Dzięki za odpowiedź, Eric. Twoje ostatnie posty na ten temat skłoniły mnie do opublikowania pytania.
Greg,
13

Wszyscy brakuje wam sensu. Powodem, dla którego rozróżnienie stosu / sterty jest ważne, jest zasięg .

struct S { ... }

void f() {
    var x = new S();
    ...
 }

Gdy x zniknie z zakresu, utworzony obiekt kategorycznie zniknie . Jest tak tylko dlatego, że jest alokowany na stosie, a nie na stosie. Nie ma nic, co mogłoby wejść w „...” część metody, która mogłaby zmienić ten fakt. W szczególności wszelkie przypisania lub wywołania metod mogły jedynie tworzyć kopie struktury S, a nie tworzyć nowych odniesień do niej, aby umożliwić jej utrzymanie.

class C { ... }

void f() {
     var x = new C();
     ...
}

Zupełnie inna historia! Ponieważ x jest teraz na stercie , jego obiekt (to znaczy sam obiekt , a nie jego kopia) mógłby równie dobrze kontynuować życie po tym, jak x zniknie z zasięgu. W rzeczywistości jedynym sposobem, w jaki nie będzie dalej istniał, jest to, że x to jedyne odniesienie do niego. Jeśli przypisania lub wywołania metod w części „...” utworzyły inne odwołania, które nadal są „aktywne” do czasu, gdy x zniknie z zakresu, obiekt ten będzie nadal działał.

To bardzo ważna koncepcja i jedynym sposobem, aby naprawdę zrozumieć „co i dlaczego”, jest poznanie różnicy między alokacją stosu i sterty.

JoelFan
źródło
Nie jestem pewien, czy widziałem ten argument przedstawiony wcześniej w książkach wraz z dyskusją o stosach / stosach, ale jest dobry. +1
Greg
2
Ze względu na sposób, w jaki C # generuje zamknięcia, kod w ...zmiennej może spowodować xkonwersję na pole klasy generowanej przez kompilator, a tym samym poza wskazany zakres. Osobiście uważam za niesmaczny pomysł niejawnego podnoszenia, ale projektanci języków wydają się go popierać (w przeciwieństwie do wymagania, aby dowolna zmienna, która została podniesiona, miała coś w deklaracji, aby to określić). Aby zapewnić poprawność programu, często konieczne jest uwzględnienie wszystkich odniesień, które mogą istnieć do obiektu. Wiedząc, że do czasu powrotu procedury, żadna kopia przekazywanego odwołania nie będzie przydatna.
supercat
1
Jeśli chodzi o „struktury są na stosie”, poprawna instrukcja jest taka, że ​​jeśli miejsce przechowywania zostanie zadeklarowane jako structType foo, miejsce przechowywania foozawiera zawartość swoich pól; jeśli foojest na stosie, podobnie są jego pola. Jeśli foojest na stercie, podobnie są jego pola. jeśli foojest w sieci Apple II, podobnie są jego pola. Natomiast jeśli foobyły typu klasy, to przytrzymaj jeden nulllub odniesienie do obiektu. Jedyną sytuacją, w której foomożna powiedzieć, że klasa jest w posiadaniu pól obiektu, byłoby, gdyby było to jedyne pole klasy i zawierało odniesienie do siebie.
supercat
+1, uwielbiam twój wgląd tutaj i myślę, że jest to poprawne ... Nie uważam jednak, aby uzasadniało to, dlaczego książki tak szczegółowo omawiają ten temat. Wydaje się, że to, co tu wyjaśniłeś, może zastąpić te 3 lub 4 rozdziały wspomnianej książki i być o wiele bardziej pomocne.
Frank V
1
z tego co wiem, struktury nie muszą, ani też nie zawsze idą na stos.
sara
5

Co do tego, dlaczego obejmują ten temat, zgadzam się z @Kirk, że jest to ważna koncepcja, którą musisz zrozumieć. Im lepiej znasz mechanizmy, tym lepiej możesz zrobić świetne aplikacje, które działają płynnie.

Teraz Eric Lippert wydaje się zgadzać z tobą, że większość autorów nie zajmuje się tym tematem. Polecam lekturę jego bloga, aby lepiej zrozumieć, co kryje się pod maską.

Fede
źródło
2
Cóż, post Erica wskazuje, że wszystko, co musisz wiedzieć, to jawna charakterystyka wartości i typów referencyjnych, i nie należy oczekiwać, że implementacja pozostanie taka sama. Myślę, że sugerowanie, że istnieje skuteczny sposób na ponowne wdrożenie C # bez stosu, jest dość pytające, ale jego racja jest poprawna: nie jest to część specyfikacji języka. Jedynym powodem, dla którego mogę skorzystać z tego wyjaśnienia, o którym mogę pomyśleć, jest to, że jest to alegoria przydatna dla programistów znających inne języki, zwłaszcza C. O ile znają alegorię, czego wiele literatury nie wyjaśnia.
Jeremy,
5

Pomyślałem, że o to właśnie chodzi w zarządzanych środowiskach. Nawet posunąłbym się do nazwania tego szczegółem implementacji podstawowego środowiska wykonawczego, o którym NIE powinieneś przyjmować żadnych założeń, ponieważ może on ulec zmianie w dowolnym momencie.

Nie wiem dużo o .NET, ale o ile wiem, jego JIT został wykonany przed wykonaniem. Na przykład JIT może przeprowadzić analizę ucieczki i co nie, a nagle będziesz mieć obiekty leżące na stosie lub tylko w niektórych rejestrach. Nie możesz tego wiedzieć.

Podejrzewam, że niektóre książki opisują to po prostu dlatego, że autorzy przywiązują do niego wielką wagę, lub ponieważ zakładają, że ich odbiorcy to robią (np. Jeśli napisałeś „C # dla programistów C ++”, prawdopodobnie powinieneś omówić ten temat).

Niemniej jednak myślę, że nie ma nic więcej do powiedzenia niż „zarządzanie pamięcią”. W przeciwnym razie ludzie mogą wyciągać błędne wnioski.

back2dos
źródło
2

Musisz zrozumieć, jak działa alokacja pamięci, aby efektywnie z niej korzystać, nawet jeśli nie musisz jawnie nią zarządzać. Dotyczy to prawie każdej abstrakcji w informatyce.

Kirk Fernandes
źródło
2
W językach zarządzanych musisz znać różnicę między typem wartości a typem odniesienia, ale poza tym łatwo jest owinąć się wokół osi, myśląc o tym, jak jest zarządzany pod maską. Zobacz tutaj na przykład: stackoverflow.com/questions/4083981/...
Robert Harvey
Muszę się zgodzić z Robertem
Różnica między przydzieloną stertą a przydzieloną stertą dokładnie wyjaśnia różnicę między wartością a typami referencji.
Jeremy,
3
@Jeremy: Nie bardzo. Zobacz blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Robert Harvey
1
Jeremy, różnica między alokacją stosu i stosu nie może wyjaśnić odmiennego zachowania między typami wartości a typami referencji, ponieważ zdarzają się sytuacje, gdy zarówno typy wartości, jak i typy referencji znajdują się na stercie, a jednak zachowują się inaczej. Ważniejszą rzeczą, którą należy zrozumieć, jest (na przykład), gdy trzeba użyć metody pass-by-referencji dla typów referencyjnych vs. typów wartości. Zależy to tylko od „czy jest to typ wartości lub typ referencyjny”, a nie „czy to jest na stercie”.
Tim Goodman,
2

Mogą istnieć pewne przypadki krawędzi, w których może to coś zmienić. Domyślne miejsce na stosie to 1 megabajt, a sterty to kilka gig. Więc jeśli twoje rozwiązanie zawiera dużą liczbę obiektów, możesz zabraknąć miejsca na stosie, mając jednocześnie dużo miejsca na stosie.

Jednak w większości jest to dość akademickie.

GrumpyMonkey
źródło
Tak, ale wątpię, by którakolwiek z tych książek starała się wyjaśnić, że same referencje są przechowywane na stosie - więc nie ma znaczenia, czy masz wiele typów referencji lub wiele typów wartości, wciąż możesz mieć przepełnienie stosu.
Jeremy,
0

Jak mówisz, C # ma wyodrębnić zarządzanie pamięcią, a alokacja sterty kontra stos są szczegółami implementacji, o których teoretycznie deweloper nie powinien wiedzieć.

Problem polega na tym, że niektóre rzeczy są naprawdę trudne do wyjaśnienia w intuicyjny sposób bez odwoływania się do tych szczegółów implementacji. Spróbuj wyjaśnić obserwowalne zachowanie podczas modyfikowania zmiennych typów wartości - jest to prawie niemożliwe, nie odwołując się do rozróżnienia stosu / sterty. Lub spróbuj wyjaśnić, dlaczego w ogóle mają typy wartości w języku i kiedy ich użyjesz? Musisz zrozumieć to rozróżnienie, aby zrozumieć język.

Zauważ, że książki o powiedzmy, że Python lub JavaScript nie robią z tego wielkiej rzeczy, nawet jeśli o tym wspominają. Wynika to z faktu, że wszystko jest albo przydzielone do stosu, albo niezmienne, co oznacza, że ​​inna semantyka kopiowania nigdy nie wchodzi w grę. W tych językach abstrakcja pamięci działa, w języku C # jest nieszczelna.

JacquesB
źródło