shared_ptr - use_count nutzen zum Feintuning.



  • Hallo zusammen

    ich habe einen Storage, der Objekte ( ~1kB ) verwaltet. Dort laufen konsequent auch Updates rein ( nicht mehr als 10 Updates/Sekunde pro Objekt ).
    Clients können diese Objekte anfordern. Im Zuge dessen mache ich von diesem Objekt eine Kopie, und transportiere diese Kopie zum Socket ( anderer Thread ), wo das Objekt in eine Message geschrieben wird. Das Objekt wird nicht verändert. Der ganze Vorgang des Kopierens und Versendens liegt im Bereich von einstelligen Mikrosekunden, d.h. ich "brauche" das Objekt nicht sehr lange.

    Nun meine Überlegung: Ich speichere das Objekt in einem shared_ptr im Storage, kopiere nur diesen shared_ptr für den Versand an den Client. Jedes mal wenn ein Update reinkommt, das das orignale Objekt verändern möchte, prüfe ich den use_count(). Ist er größer als 1 muss ich das Objekt im shared_ptr im Storage kopieren in einen neuen shared_ptr und diesen updaten. Ist er gleich 1, kann ich das Objekt im vorhandenen shared_ptr updaten, denn der Storage ist der einzige Nutzer des Objekts.

    Ziel ist die Optimierung der Anfragezeiten der Clients.
    Aus meiner Sicht hängt die Sinnhaftigkeit der Aktion von folgenden Faktoren ab:

    • Wie teuer ist das Aufrufen von use_count(). Der Aufruf darf natürlich keinesfalls das Updaten des Objekts im Storage verteuern. ich befürchte aber dass hier der Hund begraben liegt.
    • Wie teuer ist das kopieren des Objekts. Erlange ich hier im Vergleich zum Kopieren des shared_ptr überhaupt einen messbaren Vorteil.
      Beides kann ich natürlich im Performancetest nachweisen.

    Ich wollte nur erstmal vorab andere Meinungen einholen, ob diese Änderung überhaupt Sinn macht.

    gruß Tobi



  • @It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:

    Nun meine Überlegung: Ich speichere das Objekt in einem shared_ptr im Storage, kopiere nur diesen shared_ptr für den Versand an den Client. Jedes mal wenn ein Update reinkommt, das das orignale Objekt verändern möchte, prüfe ich den use_count(). Ist er größer als 1 muss ich das Objekt im shared_ptr im Storage kopieren in einen neuen shared_ptr und diesen updaten. Ist er gleich 1, kann ich das Objekt im vorhandenen shared_ptr updaten, denn der Storage ist der einzige Nutzer des Objekts.

    Ist das Ganze dann noch Thread-Safe?

    BTW: Hast du mal das Programm unter Volllast mit Programmen ala Very Sleepy geprofiled?



  • @Quiche-Lorraine sagte in shared_ptr - use_count nutzen zum Feintuning.:

    @It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:

    Nun meine Überlegung: Ich speichere das Objekt in einem shared_ptr im Storage, kopiere nur diesen shared_ptr für den Versand an den Client. Jedes mal wenn ein Update reinkommt, das das orignale Objekt verändern möchte, prüfe ich den use_count(). Ist er größer als 1 muss ich das Objekt im shared_ptr im Storage kopieren in einen neuen shared_ptr und diesen updaten. Ist er gleich 1, kann ich das Objekt im vorhandenen shared_ptr updaten, denn der Storage ist der einzige Nutzer des Objekts.

    Ist das Ganze dann noch Thread-Safe?

    Der ReferenceCounter sollte prinzipiell threadsafe sein. Mit meinem geplanten Vorgehen würde ich auch nur lesend auf den Inhalt des shared_ptr zugreifen, wenn ich jetzt keinen Denkfehler habe.
    Das Objekt wird im Fall eines Updates im Storage ja neu angelegt über den CopyCTOR des bestehenden Objekts.

    BTW: Hast du mal das Programm unter Volllast mit Programmen ala Very Sleepy geprofiled?

    Profiled hab ich das noch nicht. Wäre dann der nächste Schritt.



  • @It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:

    Der ReferenceCounter sollte prinzipiell threadsafe sein. Mit meinem geplanten Vorgehen würde ich auch nur lesend auf den Inhalt des shared_ptr zugreifen, wenn ich jetzt keinen Denkfehler habe.

    Du hast aber doch geschrieben:

    Ist er [der refcount] gleich 1, kann ich das Objekt im vorhandenen shared_ptr updaten, denn der Storage ist der einzige Nutzer des Objekts.

    Updaten ist nicht "nur lesen". Du checkst, dass der count 1 ist. Ist er. Dann kann ich ja schnell - moment, anderer Thread arbeitet, refcount ist jetzt 2 - updaten. Boom.



  • @wob sagte in shared_ptr - use_count nutzen zum Feintuning.:

    @It0101 sagte in shared_ptr - use_count nutzen zum Feintuning.:

    Der ReferenceCounter sollte prinzipiell threadsafe sein. Mit meinem geplanten Vorgehen würde ich auch nur lesend auf den Inhalt des shared_ptr zugreifen, wenn ich jetzt keinen Denkfehler habe.

    Du hast aber doch geschrieben:

    Ist er [der refcount] gleich 1, kann ich das Objekt im vorhandenen shared_ptr updaten, denn der Storage ist der einzige Nutzer des Objekts.

    Updaten ist nicht "nur lesen". Du checkst, dass der count 1 ist. Ist er. Dann kann ich ja schnell - moment, anderer Thread arbeitet, refcount ist jetzt 2 - updaten. Boom.

    Es sind zwei Threads.
    Einer betreibt den Storage. Einer den Client.
    Im Storage kommen Updates rein und auch die Anfrage aus dem Clientthread.

    Bei einem Update wird der refcount geprüft. Wenn er größer 1 ist, bedeutet das, dass irgendein Clientthread noch eine kopie vom shared_ptr in der Hand hat. D.h. ich darf den shared_ptr nicht verändern, aber lesen.
    Das nutze ich um eine Kopie vom Inhalt des shared_ptr in einem neuen shared_ptr anzulegen und direkt das Update einzuspielen und dann den bestehenden shared_ptr im Storage zu ersetzen.

    Der shared_ptr den sich der Client in dem Moment ausgeliehen hat, geht dann im refcount von 2 auf 1. Da der RefCount aber threadsafe sein sollte, sollte das kein Problem sein.



  • @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.



  • Würde es ausreichen, die Abfrage des refcount und das ggf. folgende update mit einem mutex abzusichern?



  • @Helmut-Jakoby sagte in shared_ptr - use_count nutzen zum Feintuning.:

    Würde es ausreichen, die Abfrage des refcount und das ggf. folgende update mit einem mutex abzusichern?

    Glaube ich fast nicht.

    Annahme Thread A ist gerade am versenden eines Objekts und wird unterbrochen. Thread B kommt danach an die Reihe und akualisiert just in diesem Moment das zu versendete Obekt.



  • @Quiche-Lorraine
    Stellt sich dann die Frage, warum nicht nur mit einem Objekte arbeiten. Warum eine Kopie fürs updaten erstellen und dann das Objekt im Storage ersetzen? Man könnte ja einem "Writer" fürs updaten und ggf. auch mehrere "Reader" zum lesen etablieren. Wenn der "Writer" updaten möchte, müssen halt die Reader warten.



  • Du riskierst durch fehlende "Synchronisierung bzw. MutEx" (weiß nicht, wie man das in C++ nennt) data loss und race conditions.

    Da https://www.techtarget.com/searchstorage/definition/race-condition:

    Read-modify-write. This kind of race condition happens when two processes read a value in a program and write back a new value. It often causes a software bug. Like the example above, the expectation is that the two processes will happen sequentially -- the first process produces its value and then the second process reads that value and returns a different one.

    Lesen-Ändern-Schreiben. Diese Art von Race Condition tritt auf, wenn zwei Prozesse einen Wert in einem Programm lesen und einen neuen Wert zurückschreiben. Dies verursacht oft einen Softwarefehler. Wie im obigen Beispiel wird erwartet, dass die beiden Prozesse nacheinander ablaufen - der erste Prozess erzeugt seinen Wert und der zweite Prozess liest diesen Wert und gibt einen anderen zurück.

    Beispiel:

    Oma will 100 Euro abheben.
    Gleichzeitig will die Bank 150 zwecks Lastschrift abbuchen.
    Oma liest 1000 Euro, verringert um 100 Euro und schreibt 900 zurück.
    Bank liest 1000 Euro, verringert um 150 Euro und schreibt 850 zurück.
    => Verlust von 100 Euro für die Bank.

    Es ist aber auch möglich, dass ein "ungültiger" bzw. ein nur halb aktualisierter Wert gelesen wird. In einem solchen Fall ist's wie Lotto.



  • @Helmut-Jakoby sagte in shared_ptr - use_count nutzen zum Feintuning.:

    Stellt sich dann die Frage, warum nicht nur mit einem Objekte arbeiten. Warum eine Kopie fürs updaten erstellen und dann das Objekt im Storage ersetzen? Man könnte ja einem "Writer" fürs updaten und ggf. auch mehrere "Reader" zum lesen etablieren. Wenn der "Writer" updaten möchte, müssen halt die Reader warten.

    Die Kopie ist aber auch ein Synchonisationselement. Denn der Thread B aktualisiert ja die Objekte, erstellt die Kopie und füttert damit den Versende-Thread A. Und egal was nun passiert, Thread B verändert die Kopie nicht mehr.



  • @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.



  • @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.


Log in to reply