Dlaczego inteligentne wskaźniki liczące referencje są tak popularne?

52

Jak widzę, inteligentne wskaźniki są szeroko stosowane w wielu rzeczywistych projektach C ++.

Chociaż niektóre inteligentne wskaźniki są oczywiście korzystne dla obsługi RAII i przeniesienia własności, istnieje również tendencja do domyślnego korzystania ze wskaźników wspólnych jako sposobu „wyrzucania elementów bezużytecznych” , aby programiści nie musieli tak dużo myśleć o alokacji .

Dlaczego współużytkowane wskaźniki są bardziej popularne niż integracja odpowiedniego urządzenia do usuwania śmieci, takiego jak Boehm GC ? (Czy w ogóle zgadzasz się, że są one bardziej popularne niż rzeczywiste GC?)

Wiem o dwóch zaletach tradycyjnych GC w porównaniu z liczeniem referencji:

  • Konwencjonalne algorytmy GC nie mają problemu z cyklami odniesienia .
  • Liczba referencyjna jest na ogół wolniejsza niż właściwy GC.

Jakie są powody używania inteligentnych wskaźników liczących referencje?

Miklós Homolya
źródło
6
Dodam tylko komentarz, że jest to niewłaściwe domyślne ustawienie: w większości przypadków std::unique_ptrjest wystarczające i jako takie ma zerowe obciążenie ponad surowymi wskaźnikami pod względem wydajności w czasie wykonywania. Używając std::shared_ptrwszędzie, możesz również zaciemnić semantykę własności, tracąc jedną z głównych zalet inteligentnych wskaźników innych niż automatyczne zarządzanie zasobami - jasne zrozumienie intencji kodu.
Matt
2
Przepraszamy, ale przyjęta tutaj odpowiedź jest całkowicie błędna. Zliczanie referencji ma wyższe koszty ogólne (liczba zamiast bitu znacznika i wolniejsze działanie w czasie wykonywania), nieograniczone czasy pauzy, gdy spada lawina i nie jest bardziej złożone niż, powiedzmy, półprzestrzeń Cheneya.
Jon Harrop

Odpowiedzi:

57

Niektóre zalety liczenia referencji w stosunku do odśmiecania:

  1. Niskie koszty ogólne. Urządzenia do odśmiecania mogą być dość inwazyjne (np. Powodowanie, że program zawiesza się w nieprzewidywalnych momentach, gdy trwa proces cyklu odśmiecania) i dość intensywnie zajmują pamięć (np. Ślad pamięci procesu niepotrzebnie rośnie do wielu megabajtów, zanim w końcu rozpocznie się odśmiecanie)

  2. Bardziej przewidywalne zachowanie. Dzięki liczeniu referencji masz gwarancję, że Twój obiekt zostanie uwolniony, gdy tylko ostatnie odniesienie do niego zniknie. Z drugiej strony, dzięki śmieciu, twój obiekt zostanie uwolniony „kiedyś”, kiedy system się do niego zbliży. W przypadku pamięci RAM zwykle nie jest to duży problem na komputerach stacjonarnych lub lekko obciążonych serwerach, ale w przypadku innych zasobów (np. Uchwytów plików) często trzeba je jak najszybciej zamknąć, aby uniknąć potencjalnych konfliktów później.

  3. Prostsze Zliczanie referencji można wyjaśnić za kilka minut i zaimplementować za godzinę lub dwie. Śmieciarki, szczególnie te o przyzwoitej wydajności, są niezwykle złożone i niewiele osób je rozumie.

  4. Standard. C ++ obejmuje liczenie referencji (przez shared_ptr) i znajomych w STL, co oznacza, że ​​większość programistów C ++ zna go i większość kodu C ++ będzie z nim współpracować. Nie ma jednak żadnego standardowego śmieciarza w języku C ++, co oznacza, że ​​musisz wybrać jeden i mieć nadzieję, że będzie on działał dobrze w twoim przypadku użycia - a jeśli nie, problem jest naprawiony, a nie język.

Jeśli chodzi o rzekome wady liczenia referencji - niewykrywanie cykli to problem, ale taki, którego nigdy osobiście nie spotkałem w ciągu ostatnich dziesięciu lat korzystania z liczenia referencji. Większość struktur danych ma naturalnie acykliczny charakter, a jeśli natrafisz na sytuację, w której potrzebujesz cyklicznych referencji (np. Wskaźnik nadrzędny w węźle drzewa), możesz po prostu użyć słaby_ptr lub surowego wskaźnika C dla „kierunku wstecz”. Dopóki masz świadomość potencjalnego problemu podczas projektowania struktur danych, nie stanowi to problemu.

Jeśli chodzi o wydajność, nigdy nie miałem problemu z wydajnością liczenia referencji. Miałem problemy z wydajnością wyrzucania elementów bezużytecznych, w szczególności z przypadkowymi zamrożeniami, które może powodować GC, do których jedyne rozwiązanie („nie przydzielaj obiektów”) może równie dobrze zostać przeformułowane, jak „nie używaj GC” .

Jeremy Friesner
źródło
16
Naiwne implementacje zliczania referencji zazwyczaj uzyskują znacznie niższą przepustowość niż produkcyjne GC (30–40%) kosztem opóźnień. Luka może zostać zamknięta za pomocą optymalizacji, takich jak użycie mniejszej liczby bitów do zliczania i unikanie śledzenia obiektów, dopóki nie uciekną - C ++ robi to naturalnie, jeśli głównie make_sharedpowracasz. Mimo to opóźnienia są zwykle większym problemem w aplikacjach czasu rzeczywistego, ale przepustowość jest ogólnie ważniejsza, dlatego śledzenie GC jest tak szeroko stosowane. Nie tak szybko mówiłbym o nich źle.
Jon Purdy,
3
Powiedziałbym „prostsze”: prostsze pod względem całkowitej wymaganej implementacji tak, ale nie prostsze dla kodu, który go używa : porównaj mówienie komuś, jak używać RC („rób to podczas tworzenia obiektów, a to podczas ich niszczenia” ), w jaki sposób (naiwnie, co często wystarcza) korzystać z GC („...”).
AakashM,
4
„Dzięki liczeniu referencji masz gwarancję, że Twój obiekt zostanie uwolniony, gdy tylko ostatnie odniesienie do niego zniknie”. To powszechne nieporozumienie. flyingfrogblog.blogspot.co.uk/2013/10/…
Jon Harrop
4
@JonHarrop: Ten post na blogu jest strasznie niesłuszny. Powinieneś także przeczytać wszystkie komentarze, zwłaszcza ostatni.
Deduplicator
3
@JonHarrop: Tak, jest. Nie rozumie, że życie jest pełnym zakresem, który sięga aż do zamykającego aparatu. A optymalizacja w języku F #, która według komentarzy tylko czasami działa, kończy się wcześniej, jeśli zmienna nie zostanie ponownie użyta. Co oczywiście ma swoje własne niebezpieczeństwa.
Deduplicator
26

Aby uzyskać dobrą wydajność z GC, GC musi być w stanie przenosić obiekty w pamięci. W języku takim jak C ++, w którym można bezpośrednio oddziaływać na lokalizacje pamięci, jest to prawie niemożliwe. (Microsoft C ++ / CLR się nie liczy, ponieważ wprowadza nową składnię wskaźników zarządzanych przez GC, a zatem jest efektywnie innym językiem.)

Boehm GC, choć sprytny pomysł, jest w rzeczywistości najgorszy z obu światów: potrzebujesz malloc (), który jest wolniejszy niż dobry GC, a więc tracisz deterministyczne zachowanie alokacji / dezalokacji bez odpowiedniego wzrostu wydajności generacyjnego GC . Ponadto z konieczności jest konserwatywny, więc i tak niekoniecznie zbierze wszystkie śmieci.

Dobry, dobrze dostrojony GC może być świetną rzeczą. Ale w języku takim jak C ++ zyski są minimalne, a koszty często po prostu nie są tego warte.

Ciekawe będzie jednak, gdy C ++ 11 stanie się bardziej popularny, czy lambdas i semantyka przechwytywania zaczną prowadzić społeczność C ++ w kierunku tego samego rodzaju problemów związanych z alokacją i życiem obiektów, które spowodowały, że społeczność Lisp wymyśliła GC w pierwszym miejsce.

Zobacz także moją odpowiedź na powiązane pytanie dotyczące StackOverflow .

Daniel Pryden
źródło
6
Odnośnie Boehm GC, od czasu do czasu zastanawiałem się, na ile osobiście jest odpowiedzialny za tradycyjną awersję do GC wśród programistów C i C ++, po prostu dając złe pierwsze wrażenie na temat technologii w ogóle.
Leushenko
@Leushenko Dobrze powiedziane. Przykładem jest pytanie, w którym Boehm gc nazywany jest „właściwym” gc, ignorując fakt, że jest powolny i praktycznie gwarantuje wyciek. Znalazłem to pytanie podczas badania, czy ktoś zaimplementował przerywacz cykliczny w stylu Pythona dla shared_ptr, co brzmi jak wartościowy cel dla implementacji c ++.
user4815162342,
4

Jak widzę, inteligentne wskaźniki są szeroko stosowane w wielu rzeczywistych projektach C ++.

To prawda, ale obiektywnie większość kodu jest teraz pisana w nowoczesnych językach ze śledzeniem modułów zbierających śmieci.

Chociaż pewne inteligentne wskaźniki są oczywiście korzystne dla obsługi RAII i przeniesienia własności, istnieje również tendencja do domyślnego korzystania ze wskaźników wspólnych jako sposobu „wyrzucania elementów bezużytecznych”, aby programiści nie musieli tak dużo myśleć o alokacji .

To zły pomysł, ponieważ nadal musisz martwić się o cykle.

Dlaczego współużytkowane wskaźniki są bardziej popularne niż integracja odpowiedniego urządzenia do usuwania śmieci, takiego jak Boehm GC? (Czy w ogóle zgadzasz się, że są one bardziej popularne niż rzeczywiste GC?)

Och, wow, w twoim sposobie myślenia jest tak wiele rzeczy:

  1. GC Boehm'a nie jest „właściwym” GC w żadnym znaczeniu tego słowa. To jest naprawdę okropne. Jest konserwatywny, więc przecieka i jest nieefektywny z założenia. Zobacz: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Wskaźniki udostępnione, obiektywnie, nie są tak popularne jak GC, ponieważ ogromna większość programistów używa teraz języków GC i nie potrzebuje wspólnych wskaźników. Wystarczy spojrzeć na Java i JavaScript na rynku pracy w porównaniu do C ++.

  3. Wydaje się, że ograniczasz się do C ++, ponieważ, jak zakładam, uważasz, że GC jest kwestią styczną. To nie jest ( jedynym sposobem na uzyskanie przyzwoitej GC jest zaprojektowanie dla niej języka i VM od samego początku), więc wprowadzasz błąd selekcji. Ludzie, którzy naprawdę chcą właściwego zbierania śmieci, nie trzymają się C ++.

Jakie są powody używania inteligentnych wskaźników liczących referencje?

Jesteś ograniczony do C ++, ale chciałbyś mieć automatyczne zarządzanie pamięcią.

Jon Harrop
źródło
7
Um, to pytanie oznaczone c ++, które mówi o funkcjach C ++. Oczywiście, jakieś ogólne stwierdzenia mówimy o zasięgu kodu C ++, a nie całości programowania. Jednak jednak „obiektywnie” wyrzucanie elementów bezużytecznych może być używane poza światem C ++, co w ostatecznym rozrachunku nie ma znaczenia dla tego pytania.
Nicol Bolas
2
Twój ostatni wiersz jest ewidentnie niepoprawny: jesteś w C ++ i cieszę się, że nie jesteś zmuszony do radzenia sobie z GC i opóźnia to uwalnianie zasobów. Jest powód, dla którego Apple nie lubi GC, a najważniejszą wskazówką dla języków GC jest: Nie twórz śmieci, chyba że masz dużo wolnych zasobów lub nie możesz im pomóc.
Deduplicator
3
@JonHarrop: Więc porównaj równoważne małe programy z GC i bez GC, które nie są wyraźnie wybrane do gry na korzyść którejkolwiek ze stron. Którego oczekujesz więcej pamięci?
Deduplicator
1
@Deduplicator: Mogę przewidzieć programy, które dają oba wyniki. Liczenie referencji przewyższy śledzenie GC, gdy program jest zaprojektowany do utrzymywania pamięci przydzielanej przez stertę, dopóki nie przeżyje pokoju dziecinnego (np. Kolejki list), ponieważ jest to patologiczna wydajność dla pokoleniowego GC i generuje najbardziej zmienne śmieci. Śledzenie wyrzucania elementów bezużytecznych wymagałoby mniej pamięci niż liczenie odniesień na podstawie zakresu, gdy istnieje wiele małych obiektów, a okresy istnienia są krótkie, ale nie są dobrze znane statystycznie, więc coś w rodzaju programu logicznego wykorzystującego czysto funkcjonalne struktury danych.
Jon Harrop
3
@JonHarrop: Miałem na myśli GC (śledzenie lub cokolwiek) i RAII, jeśli mówisz w C ++. Który obejmuje liczenie referencji, ale tylko tam, gdzie jest to przydatne. Lub możesz porównać z programem Swift.
Deduplicator
3

W MacOS X i iOS, a także u programistów używających Objective-C lub Swift, liczenie referencji jest popularne, ponieważ jest obsługiwane automatycznie, a użycie funkcji zbierania śmieci znacznie spadło, ponieważ Apple już go nie obsługuje (słyszę, że aplikacje używające wyrzucanie elementów bezużytecznych ulegnie awarii w następnej wersji systemu MacOS X, a odśmiecanie nigdy nie zostało zaimplementowane w systemie iOS). Naprawdę poważnie wątpię, czy kiedykolwiek było dużo oprogramowania korzystającego ze śmiecia, gdy było ono dostępne.

Powód pozbycia się śmieci: Nigdy nie działał niezawodnie w środowisku typu C, w którym wskaźniki mogły „uciekać” do obszarów niedostępnych dla śmieciarza. Apple mocno wierzy i wierzy, że liczenie referencji jest szybsze. (Możesz tutaj wysuwać roszczenia dotyczące względnej prędkości, ale nikt nie był w stanie przekonać Apple'a). I w końcu nikt nie używał śmieci.

Pierwszą rzeczą, której uczy się każdy programista MacOS X lub iOS, jest sposób obsługi cykli referencyjnych, więc nie jest to problem dla prawdziwego programisty.

gnasher729
źródło
W moim rozumieniu nie chodziło o to, że było to środowisko podobne do C, które decydowało o tym, ale że GC jest nieokreślona i potrzebuje znacznie więcej pamięci, aby uzyskać akceptowalną wydajność, a poza serwerem / pulpitem jest to zawsze trochę rzadkie.
Deduplicator
Debugowanie, dlaczego śmieciarz zniszczył obiekt, z którego wciąż korzystałem (prowadząc do awarii), zadecydowało o mnie :-)
gnasher729
O tak, to też by to zrobiło. Czy w końcu dowiedziałeś się, dlaczego?
Deduplicator
Tak, była to jedna z wielu funkcji uniksowych, w których przekazujesz void * jako „kontekst”, który jest następnie zwracany w funkcji wywołania zwrotnego; void * był naprawdę obiektem Objective-C, a śmieciarz nie zdawał sobie sprawy, że obiekt został schowany w wywołaniu Unixa. Callback jest wywoływany, rzuca void * na Object *, kaboom!
gnasher729
2

Największą wadą wyrzucania elementów bezużytecznych w C ++ jest to, że po prostu niemożliwe jest poprawne:

  • W C ++ wskaźniki nie żyją we własnej społeczności o ścianach, są mieszane z innymi danymi. W związku z tym nie można odróżnić wskaźnika od innych danych, które akurat mają wzór bitowy, który można interpretować jako prawidłowy wskaźnik.

    Konsekwencja: Dowolny moduł czyszczący C ++ wycieknie obiekty, które powinny zostać zebrane.

  • W C ++ można wykonywać arytmetykę wskaźników, aby uzyskać wskaźniki. Jako taki, jeśli nie znajdziesz wskaźnika na początku bloku, nie oznacza to, że do tego bloku nie można się odwoływać.

    Konsekwencja: Każdy śmieciarz C ++ musi wziąć pod uwagę te zmiany, traktując każdą sekwencję bitów, która zdarzy się wskazywać w dowolnym miejscu bloku, w tym tuż po jego końcu, jako prawidłowy wskaźnik, który odwołuje się do bloku.

    Uwaga: Żaden śmieciarz C ++ nie obsługuje kodu za pomocą takich sztuczek:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;
    

    To prawda, że ​​wywołuje to niezdefiniowane zachowanie. Ale część istniejącego kodu jest bardziej sprytna niż jest dla niego dobra i może spowodować wstępne zwolnienie przez moduł odśmiecający.

cmaster
źródło
2
są pomieszane z innymi danymi. Nie chodzi o to, że są „pomieszane” z innymi danymi. Łatwo jest użyć systemu typu C ++, aby zobaczyć, co jest wskaźnikiem, a co nie. Problem polega na tym, że wskaźniki często stają się innymi danymi. Ukrywanie wskaźnika w liczbie całkowitej jest niestety powszechnym narzędziem dla wielu interfejsów API w stylu C.
Nicol Bolas
1
Nie potrzebujesz nawet nieokreślonego zachowania, aby zepsuć śmietnik w c ++. Możesz na przykład serializować wskaźnik do pliku i czytać go później. W międzyczasie twój proces może nie zawierać tego wskaźnika w dowolnym miejscu w swojej przestrzeni adresowej, więc kolektor śmieci może zebrać ten obiekt, a następnie po deserializacji wskaźnika ... Ups.
Bwmat
@Bwmat „Even”? Zapisywanie wskaźników do takiego pliku wydaje się trochę ... dalekie. W każdym razie ten sam poważny problem plaguje wskaźniki do układania obiektów w stos, mogą zniknąć, gdy odczytasz wskaźnik z pliku w innym miejscu w kodzie! Deserializacja nieprawidłowej wartości wskaźnika jest niezdefiniowanym zachowaniem, nie rób tego.
hyde
Jeśli oczywiście, musisz być ostrożny, jeśli robisz coś takiego. Miał to być przykład, że ogólnie śmieciarz nie może działać „poprawnie” we wszystkich przypadkach w c ++ (bez zmiany języka)
Bwmat
1
@ gnasher729: Ehm, nie? Wskaźniki końca są w porządku?
Deduplicator