Dlaczego programiści C++ mają alergię na termin Garbage Collector?

1

One thing that that I hadn't fully understood until recently is that garbage collectors can actually allow you to write more efficient code.

Previously, I had the general understanding that you were trading convenience (not thinking about memory management or dealing with the related bugs) in exchange for performance (GC slows your program down).

That's still true broadly, but there's an interesting class of algorithms where GC can give you a perf. improvement: immutable data structures, typically used in high-concurrency situations.

Consider a concurrent hash map: when you add a new key, the old revision of the map is left unchanged (so other threads can keep reading from it), and your additions create a new revision. Each revision of the map is immutable, and your "changes" to it are really creating new, immutable copies (with tricks, to stay efficient).

These data structures are great for concurrent performance, but there's a problem: how do you know when to clean up the memory? That is: how do you know when all users are done with the old revisions, and they should be freed?

Using something like a reference count adds contention to this high-concurrency data structure, slowing it down. Threads have to fight over updating that counter, so you have now introduced shared mutable state which was the whole thing you were trying to avoid.

But if there's a GC, you don't have to think about it. And the GC can choose a "good time" to do it's bookkeeping in bulk, rather than making all of your concurrent accesses pay a price. So, if done properly, it's an overall performance win.

Interestingly, a performant solution without using GC is "hazard pointers," which are essentially like adding a teeny tiny garbage collector, devoted just to that datastructure (concurrent map, or whatever).

co sądzicie?

3

Sądzę, że autor tu opisuje problem bezpieczeństwa a nie wydajności.

how do you know when all users are done with the old revision

Najprostsza odpowiedź dzisiaj to, używam rusta, który nie skompiluje mi kodu gdzie stworzony wątek używa referencji, która może nie być we właściwym stanie w każdym momencie trwania tegoż wątku.

Each revision of the map is immutable, and your "changes" to it are really creating new, immutable copies (with tricks, to stay efficient).

Uuu, a które cudo tak robi? Nie ironicznie pytam, bo ja się zatrzymałem na wariacjach javowej ConcurrentHashMap gdzie, w uproszczeniu, blokujemy pojedyńcze kubełki zamiast całej kolejkcji, oraz takie, które robią użytek z dzielonych muteksów/blokad - shared mutex/lock.

A tak poza tym, to słaby wpis, skąd to? Takie bardziej głośne myślenie, niż jakiś educated guess. Autor teoretyzuje, że GC może pozwolić pisać efektywniejszy kod, co zawsze będzie prawdą niezależnie od problemu, tak samo jak zdanie, że kod w C może być wolny od problemów z pamięcią. Potem opisuje problem bezpieczeństwa a nie wydajności a na końcu jako argument podaje masowe zwolnienie pamięci, które to jako zaleta istnieje przynajmniej od lat 90' (techniczną świadomością nie sięgam dalej niż względnie nowoczesne IBM PC, Apple Next itp.) i trochę niepokojące, że dopiero teraz ją odkrył. Żadnych pomiarów, kodu, badań, takie sobie gdybanie.

5
several napisał(a):

Uuu, a które cudo tak robi? Nie ironicznie pytam, bo ja się zatrzymałem na wariacjach javowej ConcurrentHashMap gdzie, w uproszczeniu, blokujemy pojedyńcze kubełki zamiast całej kolejkcji, oraz takie, które robią użytek z dzielonych muteksów/blokad - shared mutex/lock.

Tu mogę się odnieść - bo temat jest mi znany z praktyki. W programowaniu funkcyjnym od dawna używa się Functional Data Structures - niestety trzeba poczytać, żeby ogarnąć (jest dużo prezentacji i video).
Paradoks tych struktur danych polega na tym, że w dowolnym małym benchmarku wyjdą oczywiście gorzej od mutowalnych (klasycznych) wersji (lista, hashmapa, etc.). Jeśli jednak wprowadzi się je w większym kodzie (np, aplikacji bankowej w javie) to okazuje się, że dostajemy zysk i na CPU i na RAM. Pierwszy raz jak to zobaczyłem to aż się zdziwiłem :-). Magii aczkolwiek nie ma.
Normalnie te struktury dają lekki narzut - powiedzmy 5-30% (w złośliwym benchmarku to i wiecej niż 200%) - tu mocno zależy co robimy.
Co ważne, wbrew powszechnym wyobrażeniom, niemutowalna lista nie oznacza, że jak chcemy coś dodać to się nie da. Da się, po prostu dostajemy nową listę. A oryginalna się nie zmienia. I nie, nie oznacza to, że kopiujemy dane. Magia?! Nie, zachęcam do poczytania.

I teraz dowcip. W praktycznie każdej większej aplikacji, gdzie mamy klasyczne mutowalne listy, mapy, gdzie miesiącami (latami) pracuje wielu ludzi, używa się defensive copying. Człowiek się szybko uczy, że bez tego tylko śledzi dziwne błędy, bywa, że dc jest wręcz wymuszane na review.
Przykład: Tworzymy nowy obiekt z listą (jako pole), który dostaje ją jako arg konstruktora - najpierw kopia, bo cholera wie co to za lista, i co ktoś z nią zaraz zrobi. Przy zwracaniu tej listy (getter) da się zwykle załatwić bez kopiowania (widok), ale zwykle też zaraz ktoś to kopiuje. W ten sposób aplikacja ciągle coś kopiuje.
(Tu się zgadzam, że to problem bezpieczeństwa, ale w znaczeniu poprawności programu).

Przejście na persistent (functional) data structures powoduje, że całe to kopiowanie nie jest już potrzebne, można bezpiecznie sobie wszędzie przekazywać struktury (szczególnie jeśli obiekty w nich są niemutowalne). I nagle wychodzi, że benchmarki o które się obawialiśmy - po wprowadzeniu niemutowalności zamiast się pogorszyć - poprawiają się istotnie.

Trudniej mi to odnieść do GC. W zasadzie to funkcyjne struktury danych wymagają jakiegoś GC, bo nie wiemy kto obiekt (listę na przykład) trzyma i możemy zwolnić dopiero jak nikt nie korzysta (oczywiście możemy użyć Rust, ale używanie niemutowalnych struktur w Rust to coś czego jeszcze sensownie nie poćwiczyłem).
Mogę zgadywać, że autor cytowany przez chinkę (:-)) odnosić się może do sytuacji - defensive copy bez GC - vs functional data structures + GC (ale to nie jest przykład, który miałem, bo moje doświadczenie dotyczyło kobył javowych z GC w obu przypadkach).

2

Przykładowo znajdź mi popularny język, gdzie masz zarówno GC i pola w strukturach, gdzie inne pola nie są koniecznie wskaźnikiami (co ma miejsce w podejściu everything is an object). C#? Go? Zgaduję, że ten pierwszy dostał ten ficzer (struktury) w okolicach 2005 co daje nam 35 lat od czasów języka C. Całkiem długo jak na taką oczywistość, która mocno wpływa na wydajność (Java też chce mieć value objecty)

C# ma struktury od samego początku, czyli okolic 2000 r. Wypominanie ile to lat po jakimś innym języku nie ma sensu, skoro mówimy o języku nowopowstałym…

1

Takie małe moje podsumowanie tego co tu przeczytałem,bo trochę się zrobiło zamieszanie przez dodatkowe wątki (jak struktury)
Ustrzeżenie: będę pisał trochę o Javie/Scali i JVMie, ale to nie dlatego uważam że od czegoś lepsze są tylko JVMa znam najlepiej z środowisk zarzadzanych

  1. GC powoduje zatrzymanie świata (stop-the-world problem) - Nie jest to zatrzymanie na cały czas odśmiecania pamięci a tylko na czas oznaczenia które obiekty powinny zostać usunięte. Potem resztę można wykonywac już równolegle. (Jednak na odwrót, jak pisze niżej @Wibowit ) Podobno jedna firma kiedyś chwaliła się że rozwiązała ten problem bo pauza u nich jest tak krótka że można to już liczyć za real-time XD

  2. Mieszanie kodu zarządzanego (przez GC) i niezarzadzanego (gdzie ręczne trzeba czyścić pamięć) - Takie rzeczy się robi i takie rzeczy są problematyczne. Ale jeśli mówimy w kontekście C++ (albo jednego języka) i wciąż chcemy mieć możliwość pisania aplikacji w całości niezarządzanych musiałoby to oznaczać że biblioteki są dostarczanwe w dwóch wersjach XD Ogrom roboty. Raczej stawiam że biblioteki byłyby pisane i tak w postaci niezarządzanej. Ale wtedy mogły by powodować wycieki pamięci w aplikacjach zarządzanych. Ewentualnie doprowadziłoby to społeczność do rozpadu na dwa. Część używającą tylko bibliotek z GC i część używającą tylko bibliotek bez GC

BTW taki mały suplement dla programistów C++ (kodu niezarządzanego). Ja rozumiem że jak w C++ trzeba zwolnić jakiś zasób deterministycznie to daje się to do destruktora. W Javie czy innych językach zarządzanych z GC może szokować to że finalizer jest wywoływany nieteterministycznie. Ale nikt nie zamyka zasobów w finalizerach (przynajmniej nie w Javie/Scali czy ogólnie na JVM, to nawet jest uważane za antypattern). Od tego jest Interfejs Autocloseable i konstrukcja Try with Resources która zastępuje deterministyczny destruktor. Można powiedzieć że połaczenia do bazy i dostęp do plików jest zarzadzany ręczie w Javie. Ale jak jakiś junior nie da rady zamknąć zasobu ręcznie to jak się zgubi (zasób, nie junior) to zamknie go GC, tylko że to trwa i mogą nam się np wyczerpać połączenia do bazy lub liczba otwartych plików (na unixie)

I to mnie prowadzi do kolejnego punktu czy akapitu że ciekawy byłby język gdzie ręcznie zarzadzamy obiektami jak w C++, ale jest jeszcze wpięty GC który potrafi czasem przejrzeć obiekty na stercie i sprawdzić czy jednak czagoś nie zgubiliśmy i nie ma wycieków. BTW2 tak, wiem że wycieki są dalej możliwe jak mamy GC

0
Pebal napisał(a):

Dlaczego programiści C++ mają alergię na termin Garbage Collector?

A mają?

0

Który język systemowy jest najbardziej podobny do Rust pod względem bezpiecznego zarządzania pamięcią? Ale jest mniejszy i prostszy o bardziej czytelnej składni?
Mam tu kilka opcji Vale, Val, V, Bog oraz bflat czyli kompilowanie C# do sprzętu. Także Zig, Odin, Jai, Roc, Toit, Haxe nie wymieniam ponieważ tak samo rozwiązują problemy z pamięcią jak C/C++.

https://github.com/Vexu/bog
https://github.com/bflattened/bflat
https://vale.dev/
https://www.val-lang.dev/
https://vlang.io/

1
Pebal napisał(a):

odnoszę nieodparte wrażenie, że środowisko to ma poważną alergię na termin Garbage Collector.

Nie sądzę, że to alergia.

  1. Owszem GC jest fajne ale da się bez tego żyć i to wciąż bardzo wygodnie;
  2. Jak masz mało zasobów a duże wymagania to tylko przeszkadza (np. embedded);
  3. Jest to dodatkowy kod, który często wykonuje się w momentach, w których tego nie chcemy. W szczególności gdy zależy nam np. na dokładnych synchronizacjach czasowych...

Jasne, że jak się pisze jakie okienka i formularze to szkoda czasu na pilnowanie pamięci ale jak już piszesz coś co zajmuje pamięć "pod korek" to czasem trzeba to zrobić ręcznie.
Jak zacząłem pisać w językach z GC to dłuuuugo jeszcze miałem nawyk ręcznego zwalniania zasobów. To wchodzi w krew i nie jest jakimś szczególnym "obciążeniem".
Oczywiście, że czasem znajdą się karkołomne sytuacje gdzie taki GC pewnie zadziała sprytniej niż mógłby to samodzielnie wymyślić programista...

W ogólności oceniam GC jako bardzo komfortowy dodatek ale jednak trochę mieszający po swojemu a ludzie piszący w takim C++ lubią czuć co program robi nie tracąc nad nim kontroli w żadnym momencie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1