Speicherverwaltung für die schnelle Nachrichtenübermittlung zwischen Threads in C ++


9

Angenommen, es gibt zwei Threads, die durch asynchrones Senden von Datennachrichten aneinander kommunizieren. Jeder Thread hat eine Art Nachrichtenwarteschlange.

Meine Frage ist sehr niedrig: Was kann erwartet werden, um den Speicher am effizientesten zu verwalten? Ich kann mir mehrere Lösungen vorstellen:

  1. Der Absender erstellt das Objekt über new. Empfängeranrufe delete.
  2. Speicherpooling (um den Speicher zurück zum Absender zu übertragen)
  3. Müllabfuhr (zB Boehm GC)
  4. (Wenn die Objekte klein genug sind) Kopieren Sie nach Wert, um die Heap-Zuordnung vollständig zu vermeiden

1) ist die naheliegendste Lösung, daher werde ich sie für einen Prototyp verwenden. Die Chancen stehen gut, dass es schon gut genug ist. Unabhängig von meinem spezifischen Problem frage ich mich jedoch, welche Technik am vielversprechendsten ist, wenn Sie die Leistung optimieren.

Ich würde erwarten, dass Pooling theoretisch das Beste ist, insbesondere weil Sie zusätzliches Wissen über den Informationsfluss zwischen den Threads verwenden können. Ich befürchte jedoch, dass es auch am schwierigsten ist, das Richtige zu finden. Viel Tuning ... :-(

Die Speicherbereinigung sollte später (nach Lösung 1) recht einfach hinzuzufügen sein, und ich würde erwarten, dass sie sehr gut funktioniert. Ich denke also, dass es die praktischste Lösung ist, wenn sich 1) als zu ineffizient herausstellt.

Wenn die Objekte klein und einfach sind, ist das Kopieren nach Wert möglicherweise am schnellsten. Ich befürchte jedoch, dass dies die Implementierung der unterstützten Nachrichten unnötig einschränkt, daher möchte ich dies vermeiden.

Antworten:


9

Wenn die Objekte klein und einfach sind, ist das Kopieren nach Wert möglicherweise am schnellsten. Ich befürchte jedoch, dass dies die Implementierung der unterstützten Nachrichten unnötig einschränkt, daher möchte ich dies vermeiden.

Wenn Sie eine Obergrenze vorhersehen können char buf[256], z. B. eine praktische Alternative, wenn Sie dies nicht können, die nur in den seltenen Fällen Heap-Zuweisungen aufruft:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

3

Dies hängt davon ab, wie Sie die Warteschlangen implementieren.

Wenn Sie sich für ein Array (Round-Robin-Stil) entscheiden, müssen Sie für Lösung 4 eine Obergrenze für die Größe festlegen. Wenn Sie sich für eine verknüpfte Warteschlange entscheiden, benötigen Sie zugewiesene Objekte.

Dann kann Ressourcenpooling leicht getan werden , wenn Sie nur die neuen ersetzen und löschen mit AllocMessage<T>und freeMessage<T>. Mein Vorschlag wäre, die Anzahl der möglichen Größen zu begrenzen Tund bei der Zuteilung von Beton aufzurunden messages.

Die direkte Speicherbereinigung kann funktionieren, aber das kann zu langen Pausen führen, wenn ein großer Teil gesammelt werden muss, und wird (glaube ich) etwas schlechter abschneiden als neu / löschen.


3

Wenn es in C ++ ist, verwenden Sie einfach einen der intelligenten Zeiger - unique_ptr würde gut für Sie funktionieren, da das zugrunde liegende Objekt erst gelöscht wird, wenn niemand ein Handle darauf hat. Sie übergeben das ptr-Objekt als Wert an den Empfänger und müssen sich keine Gedanken darüber machen, welcher Thread es löschen soll (in Fällen, in denen der Empfänger das Objekt nicht empfängt).

Sie müssten immer noch das Sperren zwischen den Threads übernehmen, aber die Leistung ist gut, da kein Speicher kopiert wird (nur das ptr-Objekt selbst, das winzig ist).

Das Zuweisen von Speicher auf dem Heap ist nicht die schnellste Aufgabe aller Zeiten. Daher wird das Pooling verwendet, um dies viel schneller zu machen. Sie nehmen einfach den nächsten Block von einem vorgroßen Heap in einem Pool und verwenden dafür einfach eine vorhandene Bibliothek .


2
Das Sperren ist normalerweise ein viel größeres Problem als das Kopieren von Speicher. Nur sagen.
tdammers

Wenn Sie schreiben unique_ptr, meinen Sie wohl shared_ptr. Obwohl die Verwendung eines intelligenten Zeigers zweifellos für die Ressourcenverwaltung gut ist, ändert dies nichts an der Tatsache, dass Sie eine Form der Speicherzuweisung und -freigabe verwenden. Ich denke, diese Frage ist eher niedrig.
5gon12eder

3

Der größte Leistungseinbruch bei der Kommunikation eines Objekts von einem Thread zu einem anderen ist der Aufwand für das Ergreifen eines Schlosses. Dies liegt in der Größenordnung von mehreren Mikrosekunden, was deutlich mehr ist als die durchschnittliche Zeit, die ein Paar von new/ deletebenötigt (in der Größenordnung von hundert Nanosekunden). Vernünftige newImplementierungen versuchen, das Sperren um fast jeden Preis zu vermeiden, um Leistungseinbußen zu vermeiden.

Sie möchten jedoch sicherstellen, dass Sie keine Sperren ergreifen müssen, wenn Sie die Objekte von einem Thread zu einem anderen kommunizieren. Ich kenne zwei allgemeine Methoden, um dies zu erreichen. Beide arbeiten nur unidirektional zwischen einem Sender und einem Empfänger:

  1. Verwenden Sie einen Ringpuffer. Beide Prozesse steuern einen Zeiger in diesen Puffer, einer ist der Lesezeiger, der andere ist der Schreibzeiger.

    • Der Absender prüft zuerst, ob Platz zum Hinzufügen eines Elements vorhanden ist, indem er die Zeiger vergleicht, fügt dann das Element hinzu und erhöht dann den Schreibzeiger.

    • Der Empfänger prüft durch Vergleichen der Zeiger, ob ein zu lesendes Element vorhanden ist, liest dann das Element und erhöht dann den Lesezeiger.

    Die Zeiger müssen atomar sein, da sie von den Threads gemeinsam genutzt werden. Jeder Zeiger wird jedoch nur von einem Thread geändert, der andere benötigt nur Lesezugriff auf den Zeiger. Die Elemente im Puffer können selbst Zeiger sein, wodurch Sie Ihren Ringpuffer einfach auf eine Größe skalieren können, die den Absenderblock nicht bildet.

  2. Verwenden Sie eine verknüpfte Liste, die immer mindestens ein Element enthält. Der Empfänger hat einen Zeiger auf das erste Element, der Sender hat einen Zeiger auf das letzte Element. Diese Zeiger werden nicht gemeinsam genutzt.

    • Der Absender erstellt einen neuen Knoten für die verknüpfte Liste und setzt seinen nextZeiger auf nullptr. Anschließend wird der nextZeiger des letzten Elements aktualisiert , um auf das neue Element zu zeigen. Schließlich speichert es das neue Element in einem eigenen Zeiger.

    • Der Empfänger überwacht den nextZeiger des ersten Elements, um festzustellen, ob neue Daten verfügbar sind. In diesem Fall wird das alte erste Element gelöscht, der eigene Zeiger auf das aktuelle Element verschoben und mit der Verarbeitung begonnen.

    In diesem Setup müssen die nextZeiger atomar sein, und der Absender muss sicherstellen, dass das vorletzte Element nicht dereferenziert wird, nachdem er seinen nextZeiger gesetzt hat. Der Vorteil ist natürlich, dass der Absender niemals sperren muss.

Beide Ansätze sind viel schneller als jeder sperrenbasierte Ansatz, erfordern jedoch eine sorgfältige Implementierung, um die richtigen Ergebnisse zu erzielen. Und natürlich erfordern sie native Hardware-Atomizität von Zeiger-Schreibvorgängen / -Ladungen; Wenn Ihre atomic<>Implementierung intern eine Sperre verwendet, sind Sie ziemlich zum Scheitern verurteilt.

Wenn Sie mehrere Leser und / oder Autoren haben, sind Sie ziemlich zum Scheitern verurteilt: Sie können versuchen, ein Schema ohne Sperren zu entwickeln, aber es wird bestenfalls schwierig sein, es zu implementieren. Diese Situationen sind mit einem Schloss viel einfacher zu handhaben. Wenn Sie jedoch eine Sperre greifen, können Sie Sorgen um stoppen new/ deleteLeistung.


+1 Ich muss diese Ringpufferlösung als Alternative zu gleichzeitigen Warteschlangen mit CAS-Schleifen auschecken. Das klingt sehr vielversprechend.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.