shared_ptr - use_count nutzen zum Feintuning.
-
Oder mal ein anderes Problem.
Nehmen wir mal an, es würde keine Kopie, sondern nur eine Referenz benutzt werden. Und es würde kein
std::shared_ptr
benutzt werden. Dann kann folgendes passieren:- Thread A ist gerade am Senden eines Objekts,
- Thread B aktualisiert die Objekte und fügt dieses dem Storage (std::vector) hinzu.
- Da der Storage intern voll ist, allokiert dieser intern neuen Speicher, kopiert die alten Werte hinein und löscht den alten Speicher
- Dadurch wird die Referenz ungültig s.d. der Thread A auf einmal Unsinn versendet.
-
@It0101 Das klingt ein wenig, als wolltest du mit dieser Aktion vor allem eine Allokation einsparen, wenn du das Objekt sicher wiederverwenden kannst.
Vielleicht macht es da alternativ mehr Sinn, bei eben diesen Allokationen anzusetzen. Du könntest z.B. mit einem Memory Pool arbeiten. Der verwaltet Speicherblöcke, die alle die selbe Größe haben und ist in der lage, sehr schnelle -Allokationen durchzuführen (einfach nächsten freien Block aus der "free list" zurückgeben).
Ab C++17 könntest du mal schauen, ob da vielleicht z.B. mit
std::pmr::unsynchronized_pool_resource
oderstd::pmr::synchronized_pool_resource
was geht. "synchronized" ist threadsicher, wahrscheinlich willst du das, es sei denn Allokationen und Deallokationen finden immer im selben Thread statt - "unsynchronized" hat da wahrscheinlich ein bisschen weniger Overhead. Ich selbst hab die noch nicht verwendet, aber ich würde das mal zuerst ausprobieren. So ein Pool hätte auch den Vorteil, dass du nicht nur Allokationen sparst, wenn es keine Kollisionen mit anderen Threads gibt, sondern das jede Allokation recht preisgünstig ist.
-
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
Der ReferenceCounter sollte prinzipiell threadsafe sein.
Ja, ist er auch. Er ist auch effizient auszulesen. Bloss gibt dir
use_count
leider nicht die ausreichenden Garantien. Siehe https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count#NotesFür micht klingt das nach einem guten Anwendungsfall für einen
boost::intrusive_ptr
. Da kannst du selbst bestimmen welche Garantien dir der Use-Count gibt und welche nicht.
-
@Schlangenmensch sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 Der Punkt von @wob ist, was passiert, wenn refcount == 1. Dann möchtest du direkt updaten.Wenn jetzt aber nach der Überprüfung auf refcount der Client kommt um sich eine Kopie des shared_ptr zu holen, befindet sich der gerade eigentlich im Update und du möchtest wahrscheinlich nicht, dass der Client damit arbeitet.
Der Fall kann nicht auftreten, da alles über eine FIFO-Queue reinkommt. Sowohl die Updates, als auch die Requests, die auch immer sofort abgearbeitet werden.
-
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 Das klingt ein wenig, als wolltest du mit dieser Aktion vor allem eine Allokation einsparen, wenn du das Objekt sicher wiederverwenden kannst.
Genau das und auch den Aufwand des Kopierens. Der MemoryPool ist keine üble Idee. Es wäre dann allerdings ein synced. den könnte ich auch für die Updates verwenden, denn im Grunde ist es immer die selbe Klasse, die im ganzen Backend verwendet wird. Sowohl für den Transport der Updates als auch für die Antworten an die Clients.
std::pmr::unsynchronized_pool_resource
oderstd::pmr::synchronized_pool_resource
Sehe ich mir an. Noch nie zuvor davon gehört.
-
Schön, das hier so eine lebhafte Diskussion entstanden ist. Danke zunächst mal für alle eure Beiträge
Mein Anwendungsfall kann ja eigentlich gar nicht so selten sein. Das Füttern eines Storage und das extrahieren von Daten daraus, ist doch eigentlich ein weitverbreiteter Fall.
-
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
ist doch eigentlich ein weitverbreiteter Fall
Ja, nur das hier noch die Netzwerkkomponente (verteilte Anwendung) und das Multi-Threading (Parallelität/Nebenläufigkeit) hinzukommen. Aber schlussendlich ist es wie Fahrradfahren oder ein Bild malen... wenn man das Handwerk einmal beherrscht, kann man es.
-
@It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
@It0101 Das klingt ein wenig, als wolltest du mit dieser Aktion vor allem eine Allokation einsparen, wenn du das Objekt sicher wiederverwenden kannst.
Genau das und auch den Aufwand des Kopierens. Der MemoryPool ist keine üble Idee. Es wäre dann allerdings ein synced. den könnte ich auch für die Updates verwenden, denn im Grunde ist es immer die selbe Klasse, die im ganzen Backend verwendet wird. Sowohl für den Transport der Updates als auch für die Antworten an die Clients.
Ja, das eigentliche "Kopieren" ohne die Allokation musst du ja auch dann machen, wenn du ein bereits vorhandenes Objekt wieder verwendest und "updatest", wie du in deinem ersten Beitrag geschrieben hast.
std::pmr::unsynchronized_pool_resource
oderstd::pmr::synchronized_pool_resource
Sehe ich mir an. Noch nie zuvor davon gehört.
Was mir bei der Doku zu diesen "pool"-Ressourcen auffällt ist, dass dort nicht wirklich Garantien zur Effizienz der Allokationen gegeben werden und dass diese auch mehrere verschiedene Blockgrößen handhaben können.
Daher mehren sich gerade meine Zweifel, ob die tatsächlich in der lage sind, den generischen Allocator zu schlagen. Immerhin müssen die auch erstmal in ihrer internen Datenstruktur die richtige Blockgröße bestimmen und wo sie den Speicher für Allokationen dieser Größe her holen. Das klingt ein klein wenig aufwändiger als einfach nur stumpf den ersten freien Block aus einer einzigen "Free List" zurückzugeben. Der generische Allocator dürfte das ähnlich handhaben und ist im Allgemeinen bereits exzellent optimiert.
Wahrscheinlich fährst du was Performance angeht mit einem dedizierten Memory Pool für nur eine einzige Blockgröße besser, z.B. sowas wie Boost.Pool.
Auch brauchst wohl du nicht unbedingt einen polymorphen Allocator wie die in
std::pmr
(der Hauptgrund für die ist, dass sie zustandsbehaftet sind und pro Instanz eigenen Speicher verwalten können, während die klassischen Allokatoren "global" arbeiten und nur "pro Allocator-Typ" individuellen Speicher haben können). Ein nicht-pmr
-Allocator tuts denke ich genau so und käme ohne den (winzigen) Overhead einer polymorphen Klasse aus.Auch empfehle ich wie @hustbaer ebenfalls
boost::intrusive_ptr
, allerdings aus etwas anderen Gründen: Wenn du einenstd::shared_pointer
erzeugst, ist dafür nicht nur eine Allokation für das Objekt selbst, sondern auch für den Kontrollblock notwendig, der den Referenzzähler enthält. Es werden also entweder zwei Allokationen mit verschiedenen Größen benötigt, oder - falls dustd::allocate_shared
, die "Allocator-Variante" vonstd::make_shared
verwendest - eine Allokation unbekannter Größe, da der Kontrollblock ein Implementations-Detail der Standardbibliothek ist (auch wenn der meist wahrscheinlich nur ein 32/24-bit Integer + eventuelles Padding für Alignment des Objekts sein dürfte). Du kannst also nicht exakt vorhersagen wie groß die Speicherblöcke des Memory Pool sein müssen. mitboost::intrusive_ptr
hättest du das Problem nicht, da dort der Referenzzähler Teil des Objekts ist.Und natürlich wie immer wenns um Performance geht: messen. Aber das sollte dir klar sein. Durchaus möglich, dass die Performance-Ausbeute gemessen an dem Aufwand nur ziemlich mager ausfällt. Aber zumindest theoretisch ist das Potential da, falls die Allokationen bei dem Code tatsächlich dominant sind.
-
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
Auch brauchst wohl du nicht unbedingt einen polymorphen Allocator wie die in
std::pmr
(der Hauptgrund für die ist, dass sie zustandsbehaftet sind und pro Instanz eigenen Speicher verwalten können, während die klassischen Allokatoren "global" arbeiten und nur "pro Allocator-Typ" individuellen Speicher haben können). Ein nicht-pmr
-Allocator tuts denke ich genau so und käme ohne den (winzigen) Overhead einer polymorphen Klasse aus.Da bin ich jetzt nicht bei Dir. Auch die „klassischen“ nicht polymorphen Allocatoren können einen Zustand haben und eigenen Speicher pro Instanz verwalten. Der Punkt ist, dass man das über die entsprechenden alloc_traits vorher testen muss, wie sich ein Allokator verhält.
Ein polymorpher Allokator verhält sich pro Instanz unterschiedlich, d.h. er kann nicht nur unterschiedliche Speicherpools verwalten, er kann für jede Instanz eine eigene Strategie verwenden.
-
@john-0 sagte in shared_ptr - use_count nutzen zum Feintuning.:
@Finnegan sagte in shared_ptr - use_count nutzen zum Feintuning.:
Auch brauchst wohl du nicht unbedingt einen polymorphen Allocator wie die in
std::pmr
(der Hauptgrund für die ist, dass sie zustandsbehaftet sind und pro Instanz eigenen Speicher verwalten können, während die klassischen Allokatoren "global" arbeiten und nur "pro Allocator-Typ" individuellen Speicher haben können). Ein nicht-pmr
-Allocator tuts denke ich genau so und käme ohne den (winzigen) Overhead einer polymorphen Klasse aus.Da bin ich jetzt nicht bei Dir. Auch die „klassischen“ nicht polymorphen Allocatoren können einen Zustand haben und eigenen Speicher pro Instanz verwalten. Der Punkt ist, dass man das über die entsprechenden alloc_traits vorher testen muss, wie sich ein Allokator verhält.
Ein polymorpher Allokator verhält sich pro Instanz unterschiedlich, d.h. er kann nicht nur unterschiedliche Speicherpools verwalten, er kann für jede Instanz eine eigene Strategie verwenden.
Ja, das stimmt. Mein Gedanke war, dass die klassischen Allokatoren keine
memory_resource
haben, Kopien des Allocator untereinander austauschbar sein müssen (Speicher, der mit einem Allocator geholt wird, muss mit der Kopie freigebbar sein) und das wegenallocator<T>::typename rebind<U>::other
auch noch über Typgrenzen hinweg funktionieren muss. Ich hatte das mental so abgespeichert, dass das nur sinnvoll mit einer gobalen Speicher-Ressource geht (wiemalloc
/free
für den Default-Allocator).Aber natürlich kann man auch einen eigenen Allocator implementieren, der ebenfalls so eine Abstraktion wie
memory_resource
verwendet, die dann zwischen den Kopien weitergereicht wird. Allerdings ist das mit denpmr
-Allokatoren deutlich einfacher, weil die diese Funktionalität bereits haben und man nur diememory_resource
implementieren muss. Daher dachte ich fälschlicherweise dass das nur mit denen ginge, aber die Motivation fürpmr
war wohl vornehmlich sowas hier zu vereinfachen:std::vector<int, A1> v1; std::vector<int, A2> v2; v1 = v2; // Inkompatible Typen
Habe ich das richtig verstanden? Die
pmr
-Allokatoren und Container lösen eigentlich 2 Probleme? Einmal die Kompatibilität von Datenstrukturen, die unterschiedliche Allokatoren verwenden und zum anderen die der gemeinsamen Speicher-Ressource die auch beim Kopieren und beirebind
weitergereicht wird (und die man mit entsprechendem Aufwand auch mit klassichen Allokatoren implementieren kann)?