shared_ptr - use_count nutzen zum Feintuning.



  • @Helmut-Jakoby

    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 O(1)\mathcal{O}(1)-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 oder std::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#Notes

    Fü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 oder std::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.


  • Gesperrt

    @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 oder std::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 einen std::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 du std::allocate_shared, die "Allocator-Variante" von std::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. mit boost::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 wegen allocator<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 (wie malloc/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 den pmr-Allokatoren deutlich einfacher, weil die diese Funktionalität bereits haben und man nur die memory_resource implementieren muss. Daher dachte ich fälschlicherweise dass das nur mit denen ginge, aber die Motivation für pmr 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 bei rebind weitergereicht wird (und die man mit entsprechendem Aufwand auch mit klassichen Allokatoren implementieren kann)?


Anmelden zum Antworten