W dzisiejszych czasach tak wiele języków jest śmieciami. Jest nawet dostępny dla C ++ przez osoby trzecie. Ale C ++ ma RAII i inteligentne wskaźniki. Po co więc używać śmiecia? Czy robi coś ekstra?
A w innych językach, takich jak C #, jeśli wszystkie referencje są traktowane jako inteligentne wskaźniki (z pominięciem RAII), według specyfikacji i implementacji, czy nadal będzie potrzebne zbieranie śmieci? Jeśli nie, to dlaczego tak nie jest?
garbage-collection
smart-pointer
Gulszan
źródło
źródło
Odpowiedzi:
Zakładam, że masz na myśli inteligentne wskaźniki zliczane według referencji i zauważę, że są one (podstawową) formą wyrzucania elementów bezużytecznych, więc odpowiem na pytanie „jakie są zalety innych form zbierania elementów bezużytecznych w porównaniu do inteligentnych wskaźników” zamiast.
Dokładność . Same liczenie referencyjne przecieka cykle, więc inteligentne wskaźniki referencyjne będą przeciekać pamięć ogólnie, chyba że zostaną dodane inne techniki w celu złapania cykli. Po dodaniu tych technik zniknęła korzyść z prostoty liczenia referencji. Należy również pamiętać, że liczenie i śledzenie referencji na podstawie zakresu gromadzi wartości GC w różnych momentach, czasem liczenie referencji zbiera się wcześniej, a czasami śledzenie GC zbiera się wcześniej.
Przepustowość . Inteligentne wskaźniki są jedną z najmniej wydajnych form odśmiecania, szczególnie w kontekście aplikacji wielowątkowych, gdy liczby referencyjne są zderzane atomowo. Istnieją zaawansowane techniki liczenia referencji zaprojektowane w celu złagodzenia tego, ale śledzenie GC nadal jest algorytmem z wyboru w środowiskach produkcyjnych.
Latencji . Typowe implementacje inteligentnych wskaźników pozwalają lawinom niszczycieli, co skutkuje nieograniczonymi czasami przerwy. Inne formy zbierania śmieci są znacznie bardziej przyrostowe i mogą nawet odbywać się w czasie rzeczywistym, np. Bieżnia Bakera.
źródło
Ponieważ nikt nie patrzył na to z tego punktu widzenia, sformułuję twoje pytanie: po co wkładać coś w język, jeśli możesz to zrobić w bibliotece? Ignorując konkretne szczegóły implementacyjne i składniowe, wskaźniki GC / smart są w zasadzie szczególnym przypadkiem tego pytania. Po co definiować moduł wyrzucania elementów bezużytecznych w samym języku, jeśli można go zaimplementować w bibliotece?
Istnieje kilka odpowiedzi na to pytanie. Najważniejsze pierwsze:
Zapewniasz, że cały kod może go używać do współdziałania. Myślę, że jest to główny powód, dla którego ponowne użycie i udostępnianie kodu nie zaczęło się tak naprawdę, dopóki Java / C # / Python / Ruby. Biblioteki muszą się komunikować, a jedynym niezawodnym wspólnym językiem, jaki mają, jest to, co znajduje się w samej specyfikacji języka (i do pewnego stopnia w standardowej bibliotece). Jeśli kiedykolwiek próbowałeś ponownie używać bibliotek w C ++, prawdopodobnie doświadczyłeś strasznego bólu, którego nie powoduje żadna standardowa semantyka pamięci. Chcę przekazać strukturę do jakiejś biblioteki lib. Czy przekazuję referencję? Wskaźnik?
scoped_ptr
?smart_ptr
? Czy przekazuję własność, czy nie? Czy istnieje sposób na wskazanie tego? Co jeśli biblioteka musi przydzielić? Czy muszę mu to przydzielić? Nie czyniąc zarządzania pamięcią częścią języka, C ++ zmusza każdą parę bibliotek, aby musiały wynegocjować tutaj swoją własną strategię i naprawdę trudno jest je wszystkie uzgodnić. GC sprawia, że jest to kompletny problem.Możesz zaprojektować składnię wokół niego. Ponieważ C ++ nie hermetyzuje samego zarządzania pamięcią, musi zapewnić szereg haczyków składniowych, aby kod na poziomie użytkownika mógł wyrażać wszystkie szczegóły. Masz wskaźniki, referencje,
const
operatory dereferencji, operatory pośrednie, adres itp. Jeśli przerzucisz zarządzanie pamięcią na sam język, składnię można zaprojektować na podstawie tego. Wszystkie te operatory znikają, a język staje się czystszy i prostszy.Otrzymujesz wysoki zwrot z inwestycji. Wartość generowana przez dany fragment kodu jest mnożona przez liczbę osób, które go używają. Oznacza to, że im więcej masz użytkowników, tym więcej możesz sobie pozwolić na wydanie oprogramowania. Po przeniesieniu funkcji na język wszyscy użytkownicy języka będą z niej korzystać. Oznacza to, że możesz włożyć w to więcej wysiłku niż w bibliotekę używaną tylko przez podzbiór tych użytkowników. Właśnie dlatego języki takie jak Java i C # mają absolutnie pierwszorzędne maszyny wirtualne i fantastycznie wysokiej jakości śmieciarki: koszty ich rozwoju są amortyzowane przez miliony użytkowników.
źródło
Dispose
wywoła obiekt zawierający mapę bitową, każde odwołanie do tego obiektu będzie odwołaniem do umieszczonego obiektu bitmapowego. Jeśli obiekt został przedwcześnie usunięty, podczas gdy inny kod nadal oczekuje jego użycia, klasa bitmap może zapewnić, że inny kod ulegnie awarii w przewidywalny sposób. Natomiast użycie odwołania do zwolnionej pamięci jest zachowaniem niezdefiniowanym.Odśmiecanie w zasadzie oznacza po prostu, że przydzielone obiekty są automatycznie zwalniane w pewnym momencie, gdy nie są już osiągalne.
Mówiąc dokładniej, są one uwalniane, gdy stają się nieosiągalne dla programu, ponieważ w przeciwnym razie obiekty o odwołaniach cyklicznych nigdy nie zostałyby uwolnione.
Inteligentne wskaźniki odnoszą się tylko do dowolnej struktury, która zachowuje się jak zwykły wskaźnik, ale ma dołączoną dodatkową funkcjonalność. Należą do nich między innymi zwolnienie, ale także kopiowanie przy zapisie, powiązane kontrole, ...
Teraz, jak już wspomniałeś, inteligentnych wskaźników można użyć do zaimplementowania formy wyrzucania elementów bezużytecznych.
Ale tok myślenia idzie w następujący sposób:
Oczywiście możesz to zaprojektować w ten sposób od samego początku. C # został zaprojektowany do zbierania śmieci, więc po prostu
new
twój obiekt i zostanie zwolniony, gdy referencje wypadną poza zakres. To, jak zostanie to zrobione, zależy od kompilatora.Ale w C ++ nie było przeznaczone wyrzucanie elementów bezużytecznych. Jeśli przydzielimy jakiś wskaźnik
int* p = new int;
i wypadnie on poza zasięg,p
sam zostanie usunięty ze stosu, ale nikt nie zajmie się przydzieloną pamięcią.Teraz jedyne, co masz od samego początku, to deterministyczne niszczyciele . Kiedy obiekt opuszcza zakres, w którym został utworzony, wywoływany jest jego destruktor. W połączeniu z szablonami i przeciążeniem operatora można zaprojektować obiekt otoki, który zachowuje się jak wskaźnik, ale wykorzystuje funkcje destruktora do czyszczenia podłączonych do niego zasobów (RAII). Nazywasz to inteligentnym wskaźnikiem .
Wszystko to jest ściśle specyficzne dla C ++: przeciążenie operatora, szablony, destruktory, ... W tej konkretnej sytuacji językowej opracowałeś inteligentne wskaźniki, które zapewnią ci GC, którego potrzebujesz.
Ale jeśli projektujesz język za pomocą GC od samego początku, jest to jedynie szczegół implementacji. Po prostu mówisz, że obiekt zostanie wyczyszczony, a kompilator zrobi to za ciebie.
Inteligentne wskaźniki, takie jak w C ++, prawdopodobnie nie byłyby nawet możliwe w językach takich jak C #, które nie mają żadnego deterministycznego zniszczenia (C # działa w tym celu, zapewniając cukier składniowy do wywoływania
.Dispose()
określonych obiektów). Zasoby niereferencyjne zostaną w końcu odzyskane przez GC, ale nie określono, kiedy dokładnie to nastąpi.A to z kolei może pozwolić GC na bardziej wydajną pracę. Wbudowany głębiej w język niż inteligentne wskaźniki, które są na nim ustawione, .NET GC może np. Opóźniać operacje pamięci i wykonywać je w blokach, aby były tańsze lub nawet przenosić pamięć w celu zwiększenia wydajności w zależności od częstotliwości obiektów są dostępne.
źródło
IDisposable
iusing
. Ale wymaga to trochę wysiłku programisty, dlatego zwykle jest używany tylko do bardzo rzadkich zasobów, takich jak uchwyty połączeń z bazą danych.IDisposable
składni, po prostu zastępując konwencjonalnylet ident = value
przezuse ident = value
...using
nie ma w ogóle nic wspólnego z odśmiecaniem pamięci, po prostu wywołuje funkcję, gdy zmienna wychodzi poza zakres, tak jak destruktory w C ++.Moim zdaniem istnieją dwie duże różnice między wyrzucaniem elementów bezużytecznych a inteligentnymi wskaźnikami wykorzystywanymi do zarządzania pamięcią:
To pierwsze oznacza, że GC będzie zbierać śmieci, których inteligentne wskaźniki nie będą; jeśli używasz inteligentnych wskaźników, musisz unikać tworzenia tego rodzaju śmieci lub być przygotowanym na ręczne radzenie sobie z nimi.
To ostatnie oznacza, że bez względu na to, jak inteligentne są inteligentne wskaźniki, ich działanie spowolni działające wątki w twoim programie. Odśmiecanie może odroczyć pracę i przenieść ją do innych wątków; co pozwala ogólnie być bardziej wydajnym (w rzeczywistości koszt czasu pracy nowoczesnego GC jest niższy niż normalny system Malloc / Free, nawet bez dodatkowego narzutu inteligentnych wskaźników) i rób to, co wciąż musi zrobić, nie wchodząc sposób wątków aplikacji.
Teraz zauważ, że inteligentne wskaźniki, będące konstrukcjami programowymi, mogą być używane do robienia różnego rodzaju innych interesujących rzeczy - patrz odpowiedź Dario - które są całkowicie poza zakresem zbierania śmieci. Jeśli chcesz to zrobić, potrzebujesz inteligentnych wskaźników.
Jednak do celów zarządzania pamięcią nie widzę żadnych perspektyw na inteligentne wskaźniki zastępujące odśmiecanie. Po prostu nie są w tym tak dobrzy.
źródło
using
blok w kolejnych wersjach C #. Co więcej, niedeterministyczne zachowanie GC może być zabraniające w systemach czasu rzeczywistego (dlatego nie stosuje się tam GC). Nie zapominajmy również, że GC są tak skomplikowane, aby naprawić, że większość z nich przecieka pamięć i są dość nieefektywne (np. Boehm…).Termin wyrzucanie elementów bezużytecznych oznacza, że do zebrania są śmieci. W C ++ inteligentne wskaźniki są dostępne w wielu odmianach, przede wszystkim w unikatowej wersji. Unique_ptr jest w zasadzie pojedynczym konstruktem własności i zakresu. W dobrze zaprojektowanym fragmencie kodu większość zasobów przydzielonych do sterty normalnie znajdowałaby się za inteligentnymi wskaźnikami Unique_ptr, a własność tych zasobów będzie zawsze dobrze zdefiniowana. W Unique_PTR nie ma prawie żadnych kosztów ogólnych, a Unique_Ptr usuwa większość problemów z ręcznym zarządzaniem pamięcią, które tradycyjnie doprowadzały ludzi do języków zarządzanych. Teraz, gdy coraz więcej współbieżnych rdzeni staje się coraz bardziej powszechnym dobrem, zasady projektowania, które skłaniają kod do używania unikalnej i dobrze zdefiniowanej własności w dowolnym momencie, stają się ważniejsze dla wydajności.
Nawet w dobrze zaprojektowanym programie, szczególnie w środowiskach wielowątkowych, nie wszystko można wyrazić bez wspólnych struktur danych, a dla tych struktur danych, które naprawdę tego wymagają, wątki muszą się komunikować. RAII w c ++ działa całkiem nieźle w kwestiach dotyczących życia w konfiguracji jednowątkowej, w konfiguracji wielowątkowej żywotność obiektów może nie być całkowicie hierarchicznie zdefiniowana. W takich sytuacjach użycie shared_ptr stanowi dużą część rozwiązania. Tworzysz współwłasność zasobu, a to w C ++ jest jedynym miejscem, w którym widzimy śmieci, ale przy tak małych ilościach, że właściwie zaprojektowany program c ++ powinien być uważany za bardziej do implementacji kolekcji śmieci ze współdzielonymi ptrami niż pełnymi śmieciami jako zaimplementowane w innych językach. C ++ po prostu nie ma tak dużo „śmieci”
Jak stwierdzili inni, inteligentne wskaźniki liczone w referencjach są jedną z form odśmiecania, a dla tego istnieje jeden poważny problem. Przykładem stosowanym głównie jako wadę zliczania odwołań w postaci śmieci, jest problem z tworzeniem osieroconych struktur danych połączonych ze sobą inteligentnymi wskaźnikami, które tworzą klastry obiektów, które uniemożliwiają się gromadzeniu. Podczas gdy w programie zaprojektowanym zgodnie z modelem obliczeniowym aktora, struktury danych zwykle nie pozwalają na pojawienie się takich nieściągalnych klastrów w C ++, gdy używasz szerokiego współdzielonego podejścia do programowania wielowątkowego, co jest używane głównie w dużej części przemysłu, osierocone klastry mogą szybko stać się rzeczywistością.
Podsumowując, jeśli przez użycie wspólnego wskaźnika masz na myśli szerokie zastosowanie unikalnego_ptr w połączeniu z modelem aktorskim obliczeń do programowania wielowątkowego i ograniczone użycie shared_ptr, niż inne formy zbierania śmieci nie kupują ci żadnych dodatkowe korzyści. Jeśli jednak podejście typu „wszystko do współużytkowania” skończyłoby się udostępnieniem w dowolnym miejscu opcji shared_ptr, należy rozważyć zmianę modeli współbieżności lub przejście na język zarządzany, który jest bardziej ukierunkowany na szersze współdzielenie własności i równoczesny dostęp do struktur danych.
źródło
Rust
że nie trzeba wyrzucać śmieci?Większość inteligentnych wskaźników jest implementowana przy użyciu liczenia referencji. Oznacza to, że każdy inteligentny wskaźnik odnoszący się do obiektu zwiększa liczbę odwołań do obiektów. Gdy liczba ta spadnie do zera, obiekt zostaje zwolniony.
Problem występuje, jeśli masz odwołania cykliczne. Oznacza to, że A ma odniesienie do B, B ma odniesienie do C, a C ma odniesienie do A. Jeśli używasz inteligentnych wskaźników, to aby zwolnić pamięć związaną z A, B i C, musisz ręcznie dostać tam „złamać” cykliczne odniesienie (np. używając
weak_ptr
w C ++).Odśmiecanie (zwykle) działa całkiem inaczej. Obecnie większość śmieciarek stosuje test osiągalności . Oznacza to, że przegląda wszystkie odwołania na stosie i te, które są globalnie dostępne, a następnie śledzi każdy obiekt, do którego odnoszą się te odwołania, i obiekty, do których się odnoszą itp. Wszystko inne to śmieci.
W ten sposób, referencje okrągłe nie liczą więcej - jak długo ani A, B i C są osiągalne , pamięć może zostać odzyskane.
Są „inne” zalety „rzeczywistego” wyrzucania śmieci. Na przykład przydzielanie pamięci jest wyjątkowo tanie: wystarczy zwiększyć wskaźnik do „końca” bloku pamięci. Dealokacja ma również stały zamortyzowany koszt. Ale oczywiście języki takie jak C ++ pozwalają na implementację zarządzania pamięcią w dowolny sposób, dzięki czemu możesz wymyślić strategię alokacji, która jest jeszcze szybsza.
Oczywiście w C ++ ilość pamięci przydzielanej na stos jest zwykle mniejsza niż język obciążony referencjami, taki jak C # / .NET. Ale to nie jest tak naprawdę kwestia zbierania śmieci w porównaniu do inteligentnych wskaźników.
W każdym razie problem nie jest taki, że wycięcie i wyschnięcie jest lepsze niż drugie. Każdy z nich ma zalety i wady.
źródło
Chodzi o wydajność . Nieprzydzielenie pamięci wymaga dużo administracji. Jeśli nieprzydzielenie działa w tle, wydajność procesu pierwszoplanowego wzrasta. Niestety przydział pamięci nie może być leniwy (przydzielone obiekty zostaną wykorzystane w świętym momencie), ale uwalnianie obiektów może.
Spróbuj w C ++ (bez GC), aby przydzielić dużą grupę obiektów, wydrukuj „cześć”, a następnie usuń je. Będziesz zaskoczony, jak długo zajmuje uwolnienie obiektów.
Ponadto GNU libc zapewnia bardziej skuteczne narzędzia do nieprzydzielania pamięci, patrz przeszkody . Należy zauważyć, że nie mam doświadczenia z przeszkodami, nigdy ich nie używałem.
źródło
Odśmiecanie może być bardziej wydajne - w zasadzie „zwiększa” koszty zarządzania pamięcią i robi to wszystko na raz. Zasadniczo spowoduje to mniejsze zużycie procesora w związku z alokacją pamięci, ale oznacza to, że w pewnym momencie będziesz mieć dużą liczbę operacji alokacji. Jeśli GC nie jest odpowiednio zaprojektowane, może stać się widoczne dla użytkownika jako „pauza”, podczas gdy GC próbuje zwolnić pamięć. Większość nowoczesnych GC bardzo dobrze utrzymuje to niewidoczne dla użytkownika, z wyjątkiem najbardziej niesprzyjających warunków.
Inteligentne wskaźniki (lub dowolny schemat liczenia referencji) mają tę zaletę, że mają miejsce dokładnie wtedy, gdy można oczekiwać od patrzenia na kod (inteligentny wskaźnik wykracza poza zakres, rzecz jest usuwana). Dostajesz małe wybuchy alokacji tu i tam. Zasadniczo możesz poświęcić więcej czasu procesorowi na alokację, ale ponieważ jest on rozłożony na wszystkie rzeczy, które dzieją się w twoim programie, jest mniej prawdopodobne (poza ograniczeniem alokacji jakiejś struktury danych potworów), aby stał się widoczny dla użytkownika.
Jeśli robisz coś, co ma znaczenie w przypadku responsywności, sugeruję, aby inteligentne liczenie wskaźników / odniesień dało Ci znać dokładnie, kiedy coś się dzieje, abyś mógł wiedzieć podczas kodowania tego, co prawdopodobnie stanie się widoczne dla użytkowników. W ustawieniach GC masz tylko najbardziej efemeryczną kontrolę nad śmieciarzem i po prostu musisz spróbować obejść ten problem.
Z drugiej strony, jeśli Twoim celem jest ogólna przepustowość, system oparty na GC może być znacznie lepszym wyborem, ponieważ minimalizuje zasoby potrzebne do zarządzania pamięcią.
Cykle: Nie uważam problemu cykli za znaczący. W systemie, w którym masz inteligentne wskaźniki, dążysz do struktur danych, które nie mają cykli, lub po prostu jesteś ostrożny, jak puścić takie rzeczy. W razie potrzeby można użyć obiektów posiadaczy, które potrafią przerwać cykle w posiadanych obiektach, aby automatycznie zapewnić odpowiednie zniszczenie. W niektórych obszarach programowania może to być ważne, ale w większości codziennych zadań nie ma znaczenia.
źródło
Ograniczeniem numer jeden inteligentnych wskaźników jest to, że nie zawsze pomagają one w stosowaniu referencji cyklicznych. Na przykład masz obiekt A przechowujący inteligentny wskaźnik do obiektu B, a obiekt B przechowuje inteligentny wskaźnik do obiektu A. Jeśli zostaną one pozostawione razem bez resetowania któregokolwiek ze wskaźników, nigdy nie zostaną zwolnione.
Dzieje się tak, ponieważ inteligentny wskaźnik musi wykonać określoną akcję, która nie będzie przetwarzana w powyższym scenariuszu, ponieważ oba obiekty są nieosiągalne dla programu. Wyrzucanie elementów bezużytecznych sobie poradzi - prawidłowo rozpozna, że obiekty nie są dostępne do programu i zostaną zebrane.
źródło
To spektrum .
Jeśli nie chcesz mieć ograniczonej wydajności i jesteś przygotowany na grindowanie, skończysz na zgromadzeniu lub c, z całym obowiązkiem ciebie do podjęcia właściwych decyzji i swobodą robienia tego, ale z tym , cała swoboda, żeby to zepsuć:
„Powiem ci, co masz robić, robisz to. Zaufaj mi”.
Śmieci to drugi koniec spektrum. Masz bardzo małą kontrolę, ale dbasz o nią:
„Powiem ci, czego chcę, spraw, aby to się stało”.
Ma to wiele zalet, głównie dlatego, że nie musisz być tak godny zaufania, jeśli chodzi o dokładną znajomość, kiedy zasób nie jest już potrzebny, ale (pomimo niektórych pływających tutaj odpowiedzi) nie jest dobry dla wydajności, i przewidywalność wydajności. (Podobnie jak wszystkie rzeczy, jeśli otrzymujesz kontrolę i robisz coś głupiego, możesz mieć gorsze wyniki. Jednak sugerowanie, że wiedza w czasie kompilacji, jakie są warunki dla uwolnienia pamięci, nie może być wykorzystana jako wygrana wydajnościowa poza naiwnością).
RAII, określanie zakresu, liczenie odwołań itp. Pomagają w poruszaniu się dalej po tym spektrum, ale nie jest tak daleko. Wszystkie te rzeczy nadal wymagają aktywnego użycia. Nadal pozwalają i wymagają interakcji z zarządzaniem pamięcią w sposób, w jaki nie działa wyrzucanie elementów bezużytecznych.
źródło
Pamiętaj, że ostatecznie wszystko sprowadza się do wykonania instrukcji przez procesor. Według mojej wiedzy wszystkie procesory klasy konsumenckiej mają zestawy instrukcji, które wymagają przechowywania danych w danym miejscu w pamięci oraz wskaźników do tych danych. To wszystko, co masz na poziomie podstawowym.
Wszystko poza tym, że ma miejsce wyrzucanie elementów bezużytecznych, odwołania do danych, które mogły zostać przeniesione, zagęszczanie sterty itp. Itd. Działa w ramach ograniczeń określonych przez powyższy paradygmat „fragmentu pamięci ze wskaźnikiem adresu”. To samo dotyczy inteligentnych wskaźników - WCIĄŻ musisz uruchomić kod na rzeczywistym sprzęcie.
źródło