Gibt es ein nichtatomares Äquivalent zu std :: shared_ptr? Und warum gibt es keinen in <memory>?

87

Dies ist eine zweiteilige Frage, die sich mit der Atomizität von std::shared_ptr:

1. Soweit ich das beurteilen kann, std::shared_ptrist dies der einzige intelligente Zeiger <memory>, der atomar ist. Ich frage mich, ob es eine nicht-atomare Version von std::shared_ptrgibt (ich kann nichts darin sehen <memory>, daher bin ich auch offen für Vorschläge außerhalb des Standards, wie die in Boost). Ich weiß, boost::shared_ptrist auch atomar (wenn BOOST_SP_DISABLE_THREADSnicht definiert), aber vielleicht gibt es eine andere Alternative? Ich suche etwas, das die gleiche Semantik hat wie std::shared_ptr, aber ohne die Atomizität.

2. Ich verstehe, warum std::shared_ptratomar ist; es ist irgendwie nett Es ist jedoch nicht für jede Situation schön, und C ++ hat in der Vergangenheit das Mantra "Bezahlen Sie nur für das, was Sie verwenden". Wenn ich nicht mehrere Threads verwende oder wenn ich mehrere Threads verwende, aber den Zeigerbesitz nicht auf mehrere Threads teile, ist ein atomarer intelligenter Zeiger übertrieben. Meine zweite Frage ist, warum std::shared_ptrin C ++ 11 keine nichtatomare Version von bereitgestellt wurde . (vorausgesetzt, es gibt ein Warum ) (wenn die Antwort einfach "eine nichtatomare Version wurde einfach nie in Betracht gezogen" oder "niemand hat jemals nach einer nichtatomaren Version gefragt" lautet, ist das in Ordnung!).

Bei Frage Nr. 2 frage ich mich, ob jemand jemals eine nicht-atomare Version von shared_ptr(entweder Boost oder dem Standardkomitee) vorgeschlagen hat (nicht um die atomare Version von zu ersetzen shared_ptr, sondern um damit zu koexistieren), und sie wurde für eine abgeschossen bestimmter Grund.

Maisstängel
quelle
4
Um welche "Kosten" geht es Ihnen hier genau? Die Kosten für die atomare Inkrementierung einer Ganzzahl? Sind das tatsächlich Kosten, die Sie für eine echte Anwendung betreffen? Oder optimieren Sie nur vorzeitig?
Nicol Bolas
8
@NicolBolas: Es ist mehr Neugier als alles andere; Ich habe (derzeit) keinen Code / kein Projekt, in dem ich ernsthaft einen nichtatomaren gemeinsamen Zeiger verwenden möchte. Ich hatte jedoch (in der Vergangenheit) Projekte, bei denen Boost's shared_ptraufgrund seiner Atomizität eine erhebliche Verlangsamung darstellte und die Definition BOOST_DISABLE_THREADSeinen spürbaren Unterschied machte (ich weiß nicht, ob std::shared_ptrdie gleichen Kosten wie diese entstanden wären boost::shared_ptr).
Cornstalks
12
@ Close Wähler: Welcher Teil der Frage ist nicht konstruktiv? Wenn es kein genaues Warum für die zweite Frage gibt, ist das in Ordnung (ein einfaches "es wurde einfach nicht berücksichtigt" wäre eine ausreichend gültige Antwort). Ich bin neugierig, ob es einen bestimmten Grund / eine bestimmte Begründung gibt. Und die erste Frage ist sicherlich eine gültige Frage, würde ich sagen. Wenn ich die Frage klären oder geringfügige Anpassungen vornehmen muss, lassen Sie es mich bitte wissen. Aber ich sehe nicht, wie es nicht konstruktiv ist.
Cornstalks
10
@Cornstalks Nun, es ist wahrscheinlich nur so, dass die Leute nicht so gut auf Fragen reagieren, die sie leicht als "vorzeitige Optimierung" abtun können , egal wie gültig, gut gestellt oder relevant die Frage ist, denke ich. Ich selbst sehe keinen Grund, dies als nicht konstruktiv zu schließen.
Christian Rau
13
( shared_ptrIch kann keine Antwort schreiben, jetzt ist es geschlossen, also kommentieren.) Wenn Ihr Programm nicht mehrere Threads verwendet, werden bei GCC keine atomaren Operationen für die Nachzählung verwendet. Unter (2) unter gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html finden Sie einen Patch für GCC, mit dem die nichtatomare Implementierung auch in Multithread-Apps für shared_ptrObjekte verwendet werden kann, die nicht gemeinsam genutzt werden Fäden. Ich habe jahrelang auf diesem Patch gesessen, aber ich denke darüber nach, ihn endlich für GCC 4.9
Jonathan Wakely

Antworten:

103

1. Ich frage mich, ob es eine nicht-atomare Version von std :: shared_ptr gibt

Nicht vom Standard bereitgestellt. Möglicherweise wird eine von einer "Drittanbieter" -Bibliothek bereitgestellt. In der Tat schien es, dass vor C ++ 11 und vor Boost jeder seinen eigenen intelligenten Zeiger mit Referenzzählung schrieb (einschließlich meiner selbst).

2. Meine zweite Frage ist, warum in C ++ 11 keine nichtatomare Version von std :: shared_ptr bereitgestellt wurde.

Diese Frage wurde auf dem Rapperswil-Treffen im Jahr 2010 erörtert. Das Thema wurde durch einen National Body Comment # 20 der Schweiz vorgestellt. Auf beiden Seiten der Debatte gab es starke Argumente, einschließlich der von Ihnen in Ihrer Frage angegebenen. Am Ende der Diskussion war die Abstimmung jedoch überwiegend (aber nicht einstimmig) gegen das Hinzufügen einer nicht synchronisierten (nicht atomaren) Version von shared_ptr.

Argumente gegen enthalten:

  • Code, der mit dem nicht synchronisierten shared_ptr geschrieben wurde, wird möglicherweise später in Thread-Code verwendet, was zu schwer zu debuggenden Problemen ohne Warnung führt.

  • Ein "universelles" shared_ptr zu haben, das die "Einbahnstraße" zum Verkehr bei der Referenzzählung darstellt, hat folgende Vorteile: Aus dem ursprünglichen Vorschlag :

    Hat unabhängig von den verwendeten Funktionen den gleichen Objekttyp, was die Interoperabilität zwischen Bibliotheken, einschließlich Bibliotheken von Drittanbietern, erheblich erleichtert.

  • Die Kosten der Atomik sind zwar nicht Null, aber nicht überwältigend. Die Kosten werden durch die Verwendung der Bewegungskonstruktion und der Bewegungszuweisung gemindert, bei denen keine atomaren Operationen verwendet werden müssen. Solche Operationen werden üblicherweise beim vector<shared_ptr<T>>Löschen und Einfügen verwendet.

  • Nichts verbietet Menschen, ihren eigenen nicht-atomaren intelligenten Zeiger mit Referenzzählung zu schreiben, wenn sie das wirklich wollen.

Das letzte Wort der LWG in Rapperswil an diesem Tag war:

CH 20 ablehnen. Derzeit besteht kein Konsens, eine Änderung vorzunehmen.

Howard Hinnant
quelle
6
Wow, perfekt, danke für die Information! Das ist genau die Art von Informationen, auf die ich gehofft hatte.
Cornstalks
> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. das ist eine extrem seltsame Argumentation. Bibliotheken von Drittanbietern stellen sowieso ihre eigenen Typen zur Verfügung. Warum sollte es also wichtig sein, wenn sie diese in Form von std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType> usw. bereitstellen? Sie müssen Ihren Code immer an das anpassen, was die Bibliothek sowieso zurückgibt
Jean-Michaël Celerier
Dies gilt für bibliotheksspezifische Typen, aber die Idee ist, dass es auch viele Stellen gibt, an denen Standardtypen in APIs von Drittanbietern angezeigt werden. Zum Beispiel könnte meine Bibliothek std::shared_ptr<std::string>irgendwohin gehen. Wenn die Bibliothek eines anderen auch diesen Typ verwendet, können Anrufer die gleichen Zeichenfolgen an uns beide weitergeben, ohne die Unannehmlichkeiten oder den Aufwand beim Konvertieren zwischen verschiedenen Darstellungen zu haben, und das ist ein kleiner Gewinn für alle.
Jack O'Connor
51

Howard hat die Frage bereits gut beantwortet, und Nicol machte einige gute Punkte zu den Vorteilen eines einzigen gemeinsamen Standardzeigertyps anstelle vieler inkompatibler Zeiger.

Obwohl ich der Entscheidung des Ausschusses voll und ganz zustimme, denke ich, dass die Verwendung eines nicht synchronisierten shared_ptrTyps in besonderen Fällen einen gewissen Vorteil hat. Deshalb habe ich das Thema einige Male untersucht.

Wenn ich nicht mehrere Threads verwende oder wenn ich mehrere Threads verwende, aber den Zeigerbesitz nicht auf mehrere Threads teile, ist ein atomarer intelligenter Zeiger übertrieben.

Wenn Ihr Programm bei GCC nicht mehrere Threads verwendet, verwendet shared_ptr keine atomaren Operationen für die Nachzählung. Dies erfolgt durch Aktualisieren der Referenzzähler über Wrapper-Funktionen, die erkennen, ob das Programm Multithreading ist (unter GNU / Linux erfolgt dies einfach durch Erkennen, ob das Programm mit verknüpft ist libpthread.so) und entsprechend an atomare oder nichtatomare Operationen gesendet werden.

Ich habe vor vielen Jahren festgestellt, dass es möglich ist, die Basisklasse mit der Single-Threaded-Sperrrichtlinie auch in Multithread-Code zu verwenden, indem sie explizit verwendet wird , da GCCs shared_ptr<T>als __shared_ptr<T, _LockPolicy>Basisklasse implementiert sind __shared_ptr<T, __gnu_cxx::_S_single>. Da dies kein beabsichtigter Anwendungsfall war, funktionierte es leider vor GCC 4.9 nicht ganz optimal, und einige Vorgänge verwendeten immer noch die Wrapper-Funktionen und wurden daher an atomare Vorgänge gesendet, obwohl Sie die _S_singleRichtlinie ausdrücklich angefordert haben. Siehe Punkt (2) unter http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlWeitere Details und einen Patch für GCC, damit die nicht-atomare Implementierung auch in Multithread-Apps verwendet werden kann. Ich habe jahrelang auf diesem Patch gesessen, ihn aber schließlich für GCC 4.9 festgelegt. Mit dieser Alias-Vorlage können Sie einen gemeinsamen Zeigertyp definieren, der nicht threadsicher, aber etwas schneller ist:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

Dieser Typ wäre nicht interoperabel std::shared_ptr<T>und nur dann sicher zu verwenden, wenn garantiert ist, dass die shared_ptr_unsynchronizedObjekte ohne zusätzliche vom Benutzer bereitgestellte Synchronisierung niemals zwischen Threads geteilt werden.

Dies ist natürlich völlig nicht portierbar, aber manchmal ist das in Ordnung. Mit den richtigen Präprozessor-Hacks würde Ihr Code mit anderen Implementierungen immer noch gut funktionieren, wenn shared_ptr_unsynchronized<T>es sich um einen Alias ​​handelt. shared_ptr<T>Mit GCC wäre er nur ein wenig schneller.


Wenn Sie ein GCC vor 4.9 verwenden, können Sie dies verwenden, indem Sie die _Sp_counted_base<_S_single>expliziten Spezialisierungen zu Ihrem eigenen Code hinzufügen (und sicherstellen, dass niemand jemals __shared_ptr<T, _S_single>ohne Einbeziehung der Spezialisierungen instanziiert , um ODR-Verstöße zu vermeiden). Das Hinzufügen solcher Spezialisierungen von stdTypen ist technisch undefiniert, würde dies aber tun Arbeiten Sie in der Praxis, denn in diesem Fall gibt es keinen Unterschied zwischen dem Hinzufügen der Spezialisierungen zu GCC oder dem Hinzufügen zu Ihrem eigenen Code.

Jonathan Wakely
quelle
2
Ich frage mich nur, gibt es in Ihrem Beispiel einen Tippfehler für den Vorlagenalias? Dh ich denke es sollte shared_ptr_unsynchronized = std :: __ shared_ptr <lauten. Übrigens habe ich dies heute in Verbindung mit std :: __ enable_shared_from_this und std :: __ schwach_ptr getestet, und es scheint gut zu funktionieren (gcc 4.9 und gcc 5.2). Ich werde es in Kürze profilieren / zerlegen, um zu sehen, ob die atomaren Operationen tatsächlich übersprungen werden.
Carl Cook
Tolle Details! Vor kurzem traf ich ein Problem, wie beschrieben in dieser Frage , die schließlich hat mich in den Quellcode zu suchen , std::shared_ptr, std::__shared_ptr, __default_lock_policyund so weiter . Diese Antwort bestätigte, was ich aus dem Code verstanden habe.
Nawaz
22

Meine zweite Frage ist, warum in C ++ 11 keine nichtatomare Version von std :: shared_ptr bereitgestellt wurde. (vorausgesetzt, es gibt ein Warum).

Man könnte genauso gut fragen, warum es keinen aufdringlichen Zeiger gibt oder eine beliebige Anzahl anderer möglicher Variationen von gemeinsam genutzten Zeigern, die man haben könnte.

Das shared_ptrvon Boost überlieferte Design bestand darin, eine Mindeststandard-Verkehrssprache für intelligente Zeiger zu schaffen. Das kann man im Allgemeinen einfach von der Wand ziehen und benutzen. Es ist etwas, das allgemein für eine Vielzahl von Anwendungen verwendet wird. Sie können es in eine Benutzeroberfläche einfügen, und die Chancen stehen gut, dass gute Leute bereit sind, es zu verwenden.

Threading wird in Zukunft nur noch häufiger auftreten . In der Tat wird das Einfädeln im Laufe der Zeit im Allgemeinen eines der wichtigsten Mittel sein, um Leistung zu erzielen. Das Erfordernis, dass der grundlegende intelligente Zeiger das Nötigste tut, um das Threading zu unterstützen, erleichtert diese Realität.

Es wäre schrecklich gewesen, ein halbes Dutzend intelligenter Zeiger mit geringfügigen Abweichungen zwischen ihnen in den Standard oder noch schlimmer in einen richtlinienbasierten intelligenten Zeiger zu integrieren. Jeder würde den Zeiger auswählen, den er am liebsten mag, und alle anderen aufgeben. Niemand würde mit jemand anderem kommunizieren können. Es wäre wie in den aktuellen Situationen mit C ++ - Zeichenfolgen, in denen jeder seinen eigenen Typ hat. Nur weitaus schlimmer, da die Interaktion mit Zeichenfolgen viel einfacher ist als die Interaktion zwischen intelligenten Zeigerklassen.

Boost und damit auch das Komitee haben einen bestimmten intelligenten Zeiger ausgewählt, der verwendet werden soll. Es bot eine gute Ausgewogenheit der Merkmale und wurde in der Praxis häufig verwendet.

std::vectorhat einige Ineffizienzen im Vergleich zu nackten Arrays in einigen Eckfällen auch. Es hat einige Einschränkungen; Einige Anwendungen möchten wirklich eine feste Grenze für die Größe von a haben vector, ohne einen Wurfzuweiser zu verwenden. Das Komitee wollte jedoch nicht vectoralles für alle sein. Es wurde als guter Standard für die meisten Anwendungen entwickelt. Diejenigen, für die es nicht funktionieren kann, können einfach eine Alternative schreiben, die ihren Bedürfnissen entspricht.

Genau wie Sie es für einen intelligenten Zeiger können, wenn shared_ptrdie Atomizität eine Belastung darstellt. Andererseits könnte man auch in Betracht ziehen, sie nicht so oft zu kopieren.

Nicol Bolas
quelle
7
+1 für "Man könnte auch in Betracht ziehen, sie nicht so oft zu kopieren."
Ali
Wenn Sie jemals einen Profiler anschließen, sind Sie etwas Besonderes und können Argumente wie die oben genannten einfach ausschalten. Wenn Sie keine betrieblichen Anforderungen haben, die schwer zu erfüllen sind, sollten Sie C ++ nicht verwenden. Streiten wie Sie ist eine gute Möglichkeit, C ++ von allen, die an hoher Leistung oder geringer Latenz interessiert sind, allgemein zu verleumden. Aus diesem Grund verwenden Spielprogrammierer keine STL, Boost oder sogar Ausnahmen.
Hans Malherbe
Aus Gründen der Klarheit, ich denke , das Zitat am Anfang Ihrer Antwort lesen sollte „ warum nicht eine nicht-atomare Version von std :: shared_ptr bereitgestellt in C ++ 11?“
Charles Savoie
4

Ich bereite einen Vortrag über shared_ptr bei der Arbeit vor. Ich habe einen modifizierten Boost shared_ptr verwendet, um ein separates Malloc zu vermeiden (wie es make_shared kann), und einen Vorlagenparameter für die oben erwähnte Sperrrichtlinie wie shared_ptr_unsynchronized. Ich benutze das Programm von

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

als Test nach dem Bereinigen der unnötigen shared_ptr-Kopien. Das Programm verwendet nur den Hauptthread und das Testargument wird angezeigt. Die Testumgebung ist ein Notebook mit Linuxmint 14. Hier ist die in Sekunden benötigte Zeit:

Testlauf Setup Boost (1.49) Standard mit make_shared modifiziertem Boost
mt-unsicher (11) 11.9 9 / 11.5 (-pthread on) 8.4  
Atom (11) 13,6 12,4 13,0  
mt-unsicher (12) 113,5 85,8 / 108,9 (-pthread on) 81,5  
Atom (12) 126,0 109,1 123,6  

Nur die 'std'-Version verwendet -std = cxx11, und der -pthread wechselt wahrscheinlich lock_policy in der Klasse g ++ __shared_ptr.

Anhand dieser Zahlen sehe ich die Auswirkungen atomarer Anweisungen auf die Codeoptimierung. Der Testfall verwendet keine C ++ - Container, vector<shared_ptr<some_small_POD>>leidet jedoch wahrscheinlich, wenn das Objekt keinen Thread-Schutz benötigt. Boost leidet weniger wahrscheinlich, weil das zusätzliche Malloc das Inlining und die Codeoptimierung begrenzt.

Ich habe noch keine Maschine mit genügend Kernen gefunden, um die Skalierbarkeit von atomaren Anweisungen einem Stresstest zu unterziehen, aber es ist wahrscheinlich besser, std :: shared_ptr nur bei Bedarf zu verwenden.

russ
quelle
3

Boost bietet eine shared_ptrnicht atomare. Es heißt local_shared_ptrund befindet sich in der Smart-Pointer-Bibliothek von Boost.

Der Quantenphysiker
quelle
+1 für eine kurze, solide Antwort mit gutem Zitat, aber dieser Zeigertyp sieht aufgrund einer zusätzlichen Indirektionsebene (local-> shared-> ptr vs shared-> ptr) teuer aus - sowohl in Bezug auf den Speicher als auch auf die Laufzeit.
Red.Wave
@ Red.Wave Können Sie erklären, was Sie mit Indirektion meinen und wie sich dies auf die Leistung auswirkt? Meinst du, dass es shared_ptrsowieso ein Zähler ist, obwohl es lokal ist? Oder meinst du damit ein anderes Problem? Die Dokumente sagen, dass der einzige Unterschied darin besteht, dass dies nicht atomar ist.
Der Quantenphysiker
Jeder lokale ptr zählt und verweist auf den ursprünglich gemeinsam genutzten ptr. Daher benötigt jeder Zugriff auf den endgültigen Pointee eine Dereferenzierung von lokalem zu gemeinsam genutztem ptr, die dann eine Dereferenzierung zum Pointee ist. Somit gibt es eine weitere Indirektion, die bis zu den Indirektionen von gemeinsam genutztem ptr gestapelt ist. Und das erhöht den Overhead.
Red.Wave
@ Red.Wave Woher bekommen Sie diese Informationen? Dies: "Daher muss für jeden Zugriff auf den endgültigen Pointee eine Dereferenzierung von lokalem zu gemeinsam genutztem PTR durchgeführt werden." Ich konnte das in Boost-Dokumenten nicht finden. Wiederum habe ich in den Dokumenten gesehen, dass es das sagt local_shared_ptrund shared_ptrbis auf Atomic identisch ist. Ich bin wirklich daran interessiert herauszufinden, ob das, was Sie sagen, wahr ist, weil ich es local_shared_ptrin Anwendungen verwende, die eine hohe Leistung erfordern.
Der Quantenphysiker
3
@ Red.Wave Wenn Sie sich den tatsächlichen Quellcode github.com/boostorg/smart_ptr/blob/… ansehen, werden Sie feststellen, dass es keine doppelte Indirektion gibt. Dieser Absatz in der Dokumentation ist nur ein mentales Modell.
Ilya Popov