Warum sind intelligente Zeiger mit Referenzzählung so beliebt?

52

Wie ich sehen kann, werden intelligente Zeiger in vielen realen C ++ - Projekten häufig verwendet.

Obwohl eine Art von intelligenten Zeigern offensichtlich für die Unterstützung von RAII- und Eigentumsübertragungen von Vorteil ist, gibt es auch einen Trend, standardmäßig gemeinsame Zeiger als " Speicherbereinigungsmethode " zu verwenden , damit der Programmierer nicht so viel über die Zuordnung nachdenken muss .

Warum sind geteilte Zeiger beliebter als die Integration eines richtigen Müllsammlers wie Boehm GC ? (Oder stimmen Sie überhaupt zu, dass sie beliebter sind als die eigentlichen GCs?)

Ich kenne zwei Vorteile herkömmlicher GCs gegenüber der Referenzzählung:

  • Herkömmliche GC-Algorithmen haben kein Problem mit Referenzzyklen .
  • Die Referenzzählung ist im Allgemeinen langsamer als eine richtige GC.

Was sind die Gründe für die Verwendung von intelligenten Zeigern mit Referenzzählung?

Miklós Homolya
quelle
6
Ich möchte nur einen Kommentar hinzufügen, der besagt, dass dies eine falsche Standardeinstellung ist: In den meisten Fällen std::unique_ptrist dies ausreichend und daher ist der Overhead für Raw-Zeiger in Bezug auf die Laufzeitleistung gleich Null. Durch die Verwendung von " std::shared_ptrÜberall" wird auch die Besitzersemantik verdeckt, und einer der Hauptvorteile von intelligenten Zeigern als die automatische Ressourcenverwaltung entfällt.
Matt
2
Sorry, aber die hier akzeptierte Antwort ist völlig falsch. Die Referenzzählung hat einen höheren Overhead (eine Zählung anstelle eines Markierungsbits und eine langsamere Laufzeitleistung), unbegrenzte Pausenzeiten, wenn die Lawine dekrementiert wird, und keine komplexere, beispielsweise Cheney-Halbraum.
Jon Harrop

Antworten:

57

Einige Vorteile der Referenzzählung gegenüber der Speicherbereinigung:

  1. Geringer Overhead. Garbage Collectors können sehr aufdringlich sein (z. B. das Einfrieren Ihres Programms zu unvorhersehbaren Zeiten, während ein Garbage Collection-Zyklus abläuft) und sehr speicherintensiv (z. B. wächst der Speicherbedarf Ihres Prozesses unnötigerweise auf viele Megabyte, bevor die Garbage Collection endlich einsetzt).

  2. Vorhersehbares Verhalten. Mit der Referenzzählung können Sie sicher sein, dass Ihr Objekt sofort freigegeben wird, wenn die letzte Referenz darauf nicht mehr vorhanden ist. Bei der Garbage Collection hingegen wird Ihr Objekt "irgendwann" freigegeben, wenn das System sich darum kümmert. Für RAM ist dies normalerweise kein großes Problem auf Desktops oder Servern mit geringer Auslastung. Für andere Ressourcen (z. B. Dateihandles) müssen diese jedoch häufig so schnell wie möglich geschlossen werden, um mögliche spätere Konflikte zu vermeiden.

  3. Einfacher. Die Referenzzählung kann in wenigen Minuten erklärt und in ein oder zwei Stunden implementiert werden. Abfallsammler, insbesondere solche mit anständiger Leistung, sind äußerst komplex und werden von wenigen verstanden.

  4. Standard. C ++ enthält Referenzzählung (über shared_ptr) und Freunde in der STL, was bedeutet, dass die meisten C ++ - Programmierer damit vertraut sind und der meiste C ++ - Code damit arbeiten wird. Es gibt jedoch keinen Standard-C ++ - Garbage-Collector, was bedeutet, dass Sie einen auswählen müssen und hoffen, dass er für Ihren Anwendungsfall gut funktioniert - und wenn dies nicht der Fall ist, müssen Sie das Problem beheben, nicht die Sprache.

Was die angeblichen Nachteile des Referenzzählens angeht - es ist ein Problem, Zyklen nicht zu erkennen, das ich in den letzten zehn Jahren bei der Verwendung des Referenzzählens noch nie persönlich erlebt habe. Die meisten Datenstrukturen sind von Natur aus azyklisch, und wenn Sie auf eine Situation stoßen, in der Sie zyklische Referenzen benötigen (z. B. übergeordneter Zeiger in einem Baumknoten), können Sie einfach einen weak_ptr- oder einen rohen C-Zeiger für die "Rückwärtsrichtung" verwenden. Solange Sie das potenzielle Problem beim Entwerfen Ihrer Datenstrukturen kennen, ist dies kein Problem.

Was die Leistung betrifft, hatte ich nie ein Problem mit der Leistung der Referenzzählung. Ich hatte Probleme mit der Leistung der Garbage Collection, insbesondere mit den zufälligen Einfrierungen, die GC verursachen kann. Die einzige Lösung ("Objekte nicht zuweisen") könnte genauso gut umformuliert werden wie "GC nicht verwenden". .

Jeremy Friesner
quelle
16
Naive Referenzzählungsimplementierungen erzielen auf Kosten der Latenz in der Regel einen viel geringeren Durchsatz als Produktions-GCs (30–40%). Die Lücke kann durch Optimierungen geschlossen werden, z. B. durch die Verwendung weniger Bits für die Nachzählung und das Vermeiden des Verfolgens von Objekten, bis sie entkommen. C ++ führt dies natürlich aus, wenn Sie hauptsächlich make_sharedbei der Rückgabe vorgehen . Dennoch ist die Latenz in Echtzeitanwendungen tendenziell das größere Problem, aber der Durchsatz ist im Allgemeinen wichtiger, weshalb Trace-GCs so häufig eingesetzt werden. Ich würde nicht so schnell schlecht über sie sprechen.
Jon Purdy
3
Ich würde "einfacher" streiten: Einfacher in Bezug auf die Gesamtmenge der erforderlichen Implementierung, ja, aber nicht einfacher für den Code, der es verwendet: Vergleichen Sie, wie jemand RC verwendet ("Tun Sie dies beim Erstellen von Objekten und dies beim Zerstören von Objekten "). ), wie man (naiv, was oft genug ist) GC benutzt ('...').
AakashM
4
"Mit der Referenzzählung können Sie sicher sein, dass Ihr Objekt in dem Moment freigegeben wird, in dem die letzte Referenz darauf wegfällt." Das ist ein weit verbreitetes Missverständnis. flyingfrogblog.blogspot.co.uk/2013/10/…
Jon Harrop
4
@ JonHarrop: Dieser Blog-Post ist fürchterlich falsch. Sie sollten auch alle Kommentare lesen , insbesondere den letzten.
Deduplizierer
3
@ JonHarrop: Ja, das gibt es. Er versteht nicht, dass die Lebensspanne der volle Umfang ist, der bis zur abschließenden Klammer reicht. Und die Optimierung in F #, die laut den Kommentaren nur manchmal funktioniert, beendet die Lebensdauer früher, wenn die Variable nicht wieder verwendet wird. Welches natürlich seine eigenen Gefahren hat.
Deduplizierer
26

Um eine gute Leistung eines GC zu erzielen, muss der GC in der Lage sein, Objekte im Speicher zu verschieben. In einer Sprache wie C ++, in der Sie direkt mit Speicherorten interagieren können, ist dies so gut wie unmöglich. (Microsoft C ++ / CLR zählt nicht, da es eine neue Syntax für GC-verwaltete Zeiger einführt und somit effektiv eine andere Sprache ist.)

Der Boehm-GC ist zwar eine clevere Idee, aber tatsächlich das Schlimmste aus beiden Welten: Sie benötigen ein malloc (), das langsamer als ein guter GC ist, und verlieren daher das deterministische Zuordnungs- / Freigabeverhalten ohne den entsprechenden Leistungsschub eines Generations-GC . Außerdem ist es zwangsläufig konservativ, sodass nicht unbedingt der gesamte Müll eingesammelt wird.

Ein guter, gut abgestimmter GC kann eine großartige Sache sein. Aber in einer Sprache wie C ++ sind die Gewinne minimal und die Kosten sind es oft nicht wert.

Mit zunehmender Beliebtheit von C ++ 11 wird es jedoch interessant sein zu sehen, ob Lambdas und Capture-Semantik die C ++ - Community zu denselben Zuordnungs- und Objektlebensdauerproblemen führen, die die Lisp-Community veranlasste, GCs zu erfinden Ort.

Siehe auch meine Antwort auf eine verwandte Frage über StackOverflow .

Daniel Pryden
quelle
6
Was den Böhm GC angeht, habe ich mich gelegentlich gefragt, inwieweit er persönlich für die traditionelle Abneigung gegen GC bei C- und C ++ - Programmierern verantwortlich ist, indem er einfach einen schlechten ersten Eindruck von der Technologie im Allgemeinen vermittelt.
Leushenko
@Leushenko Gut gesagt. Ein typisches Beispiel ist diese Frage, bei der Boehm gc als "richtiger" gc bezeichnet wird, wobei die Tatsache ignoriert wird, dass er langsam ist und praktisch garantiert ausläuft. Ich habe diese Frage gefunden, als ich nachforschte, ob jemand einen Python-ähnlichen Cycle Breaker für shared_ptr implementiert hat, was sich nach einem lohnenden Ziel für eine C ++ - Implementierung anhört.
user4815162342
4

Wie ich sehen kann, werden intelligente Zeiger in vielen realen C ++ - Projekten häufig verwendet.

Richtig, aber objektiv gesehen ist die überwiegende Mehrheit des Codes jetzt in modernen Sprachen geschrieben, wobei die Garbage Collectors nachverfolgt werden.

Obwohl eine Art von intelligenten Zeigern offensichtlich für die Unterstützung von RAII- und Eigentumsübertragungen von Vorteil ist, gibt es auch einen Trend, standardmäßig gemeinsame Zeiger als "Speicherbereinigungsmethode" zu verwenden, damit der Programmierer nicht so viel über die Zuordnung nachdenken muss .

Das ist eine schlechte Idee, weil Sie sich immer noch um die Zyklen kümmern müssen.

Warum sind geteilte Zeiger beliebter als die Integration eines richtigen Müllsammlers wie Boehm GC? (Oder stimmen Sie überhaupt zu, dass sie beliebter sind als die eigentlichen GCs?)

Oh wow, es gibt so viele Dinge, die an deiner Denkweise falsch sind:

  1. Böhms GC ist im wahrsten Sinne des Wortes kein "richtiger" GC. Es ist wirklich schrecklich. Es ist konservativ, so dass es ausläuft und konstruktionsbedingt ineffizient ist. Siehe: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Geteilte Zeiger sind objektiv gesehen bei weitem nicht so beliebt wie GC, da die überwiegende Mehrheit der Entwickler jetzt GC-basierte Sprachen verwendet und keine gemeinsamen Zeiger benötigt. Schauen Sie sich Java und Javascript auf dem Arbeitsmarkt im Vergleich zu C ++ an.

  3. Sie scheinen die Betrachtung auf C ++ zu beschränken, da Sie, wie ich annehme, glauben, dass GC ein tangentiales Problem ist. Dies ist nicht der Fall (der einzige Weg, um einen anständigen GC zu erhalten, besteht darin, die Sprache und die VM von Anfang an dafür zu entwerfen). Sie führen also eine Auswahlverzerrung ein. Leute, die wirklich eine ordnungsgemäße Garbage Collection wollen, bleiben nicht bei C ++.

Was sind die Gründe für die Verwendung von intelligenten Zeigern mit Referenzzählung?

Sie sind auf C ++ beschränkt, wünschen sich jedoch eine automatische Speicherverwaltung.

Jon Harrop
quelle
7
Ähm, es ist eine Frage mit dem Tag c ++, die sich mit C ++ - Funktionen befasst. Offensichtlich keine allgemeinen Aussagen sprechen über in C ++ Code, nicht die Gesamtheit der Programmierung. So kann jedoch "objektiv" Garbage Collection außerhalb der C ++ - Welt verwendet werden, was letztendlich für die vorliegende Frage irrelevant ist.
Nicol Bolas
2
Ihre letzte Zeile ist offensichtlich falsch: Sie befinden sich in C ++ und sind froh, dass Sie nicht gezwungen sind, sich mit GC zu befassen, und dass sich die Freigabe von Ressourcen verzögert. Es gibt einen Grund, warum Apple GC nicht mag, und die wichtigste Richtlinie für GC-Sprachen lautet: Erstellen Sie keinen Müll, es sei denn, Sie verfügen über unzureichende Ressourcen oder können nichts dagegen tun.
Deduplizierer
3
@JonHarrop: Vergleichen Sie also äquivalente kleine Programme mit und ohne GC, die nicht explizit ausgewählt wurden, um für beide Seiten von Vorteil zu sein. Welchen erwarten Sie, mehr Speicher zu benötigen?
Deduplicator
1
@ Deduplicator: Ich kann mir Programme vorstellen, die beide Ergebnisse liefern. Die Referenzzählung ist besser als die GC-Verfolgung, wenn das Programm so konzipiert ist, dass der Heap-Allokationsspeicher so lange erhalten bleibt, bis er das Kinderzimmer überlebt (z. B. eine Warteschlange von Listen), da dies für einen GC der Generation pathologisch ist und den schwebendsten Müll erzeugt. Das Verfolgen der Speicherbereinigung würde weniger Speicherplatz erfordern als die bereichsbezogene Referenzzählung, wenn viele kleine Objekte vorhanden sind und die Lebensdauern kurz, aber statisch nicht gut bekannt sind, also so etwas wie ein Logikprogramm, das rein funktionale Datenstrukturen verwendet.
Jon Harrop
3
@ JonHarrop: Ich meinte mit GC (Tracing oder was auch immer) und RAII, wenn Sie C ++ sprechen. Das beinhaltet Referenzzählung, aber nur, wo es nützlich ist. Oder Sie könnten mit einem Swift-Programm vergleichen.
Deduplizierer
3

In MacOS X und iOS und bei Entwicklern, die Objective-C oder Swift verwenden, ist die Referenzzählung beliebt, da sie automatisch verarbeitet wird und die Verwendung der Müllabfuhr erheblich reduziert wurde, da Apple sie nicht mehr unterstützt (es wird mir mitgeteilt, dass Apps verwendet werden) Die Garbage Collection wird in der nächsten MacOS X-Version nicht mehr funktionieren, und die Garbage Collection wurde in iOS nie implementiert. Ich bezweifle wirklich ernsthaft, dass es jemals viel Software gab, die Garbage Collection verwendete, als es verfügbar war.

Der Grund für die Beseitigung der Garbage Collection: In einer Umgebung im C-Stil, in der Zeiger in Bereiche "entweichen" könnten, auf die der Garbage Collector keinen Zugriff hat, funktionierte dies nie zuverlässig. Apple ist der festen Überzeugung, dass die Referenzzählung schneller ist. (Sie können hier keine Angaben zur relativen Geschwindigkeit machen, aber niemand hat Apple überzeugen können). Und am Ende nutzte niemand die Müllabfuhr.

Das erste, was jeder MacOS X- oder iOS-Entwickler lernt, ist der Umgang mit Referenzzyklen. Für einen echten Entwickler ist das also kein Problem.

gnasher729
quelle
So wie ich das verstehe, war es nicht so, dass es sich um eine C-ähnliche Umgebung handelte, die die Dinge entschied, sondern dass GC unbestimmt ist und viel mehr Speicher benötigt, um eine akzeptable Leistung zu erzielen, und dass Server / Desktop außerhalb immer etwas knapp sind.
Deduplikator
Das Debuggen, warum der Müllmann ein Objekt zerstört hat, das ich noch verwendet habe (was zu einem Absturz führte), hat es für mich entschieden :-)
gnasher729
Oh ja, das würde es auch tun. Hast du am Ende herausgefunden warum?
Deduplikator
Ja, es war eine von vielen Unix-Funktionen, bei denen Sie eine Lücke * als "Kontext" übergeben, die Ihnen dann in einer Rückruffunktion zurückgegeben wird. Die Leere * war wirklich ein Objective-C-Objekt, und der Garbage Collector wusste nicht, dass das Objekt im Unix-Aufruf versteckt war. Callback wird aufgerufen, wirft ungültig * auf Object *, kaboom!
gnasher729
2

Der größte Nachteil der Garbage Collection in C ++ ist, dass es unmöglich ist, richtig zu machen:

  • In C ++ leben Zeiger nicht in einer eigenen Walled Community, sondern werden mit anderen Daten gemischt. Als solches können Sie einen Zeiger nicht von anderen Daten unterscheiden, die zufällig ein Bitmuster haben, das als gültiger Zeiger interpretiert werden kann.

    Konsequenz: In jedem C ++ - Garbage Collector werden Objekte gelöscht, die gesammelt werden sollten.

  • In C ++ können Sie Zeigerarithmetik ausführen, um Zeiger abzuleiten. Wenn Sie also keinen Zeiger auf den Anfang eines Blocks finden, bedeutet dies nicht, dass auf diesen Block nicht verwiesen werden kann.

    Konsequenz: Jeder C ++ - Garbage Collector muss diese Anpassungen berücksichtigen und jede Bitsequenz, die auf eine beliebige Stelle innerhalb eines Blocks zeigt, einschließlich unmittelbar danach, als gültigen Zeiger behandeln, der auf den Block verweist.

    Hinweis: Kein C ++ - Garbage Collector kann Code mit folgenden Tricks verarbeiten:

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

    Richtig, dies ruft undefiniertes Verhalten hervor. Aber ein vorhandener Code ist schlauer als gut für ihn und kann eine vorläufige Freigabe durch einen Garbage Collector auslösen.

cmaster
quelle
2
" Sie werden mit anderen Daten gemischt. " Es ist nicht so sehr, dass sie mit anderen Daten "gemischt" werden. Mit dem C ++ - Typensystem können Sie leicht erkennen, was ein Zeiger ist und was nicht. Das Problem ist, dass Zeiger häufig zu anderen Daten werden. Das Ausblenden eines Zeigers in einer Ganzzahl ist für viele C-APIs leider ein weit verbreitetes Tool.
Nicol Bolas
1
Sie brauchen nicht einmal undefiniertes Verhalten, um einen Garbage Collector in c ++ zu vermasseln. Sie können beispielsweise einen Zeiger auf eine Datei serialisieren und später einlesen. In der Zwischenzeit enthält Ihr Prozess möglicherweise nirgendwo in seinem Adressraum diesen Zeiger, sodass der Garbage Collector dieses Objekt sammeln könnte, und dann, wenn Sie den Zeiger deserialisieren ... Whoops.
Bwmat
@Bwmat "Gerade"? Das Schreiben von Zeigern auf eine solche Datei scheint ein bisschen ... weit hergeholt zu sein. Wie auch immer, dasselbe ernste Problem plagt Zeiger auf Stapelobjekte. Sie könnten verschwunden sein, wenn Sie den Zeiger an einer anderen Stelle im Code aus einer Datei zurücklesen! Das Deserialisieren eines ungültigen Zeigerwerts ist ein undefiniertes Verhalten. Tun Sie das nicht.
Hyde
Wenn Sie so etwas tun, müssen Sie natürlich vorsichtig sein. Es sollte ein Beispiel sein, dass ein Garbage Collector in c ++ im Allgemeinen nicht in allen Fällen 'richtig' funktionieren kann (ohne die Sprache zu ändern)
Bwmat
1
@ gnasher729: Ähm, nein? Vergangene-End-Zeiger sind völlig in Ordnung?
Deduplizierer