OutOfLine - In-Memory-Muster für leistungsstarke C ++ - Anwendungen

Während meiner Arbeit bei Headlands Technologies hatte ich das Glück, mehrere Dienstprogramme zu schreiben, um die Erstellung von Hochleistungs-C ++ - Code zu vereinfachen. Dieser Artikel bietet einen allgemeinen Überblick über eines dieser Dienstprogramme, OutOfLine .


Beginnen wir mit einem anschaulichen Beispiel. Angenommen, Sie haben ein System, das eine große Anzahl von Dateisystemobjekten verarbeitet. Dies können normale Dateien sein, die als UNIX-Sockets oder Pipes bezeichnet werden. Aus irgendeinem Grund öffnen Sie beim Start viele Dateideskriptoren, arbeiten dann intensiv mit ihnen und schließen am Ende die Deskriptoren und löschen Links zu Dateien (ca. Die Spur bedeutet die Funktion zum Aufheben der Verknüpfung).


Die ursprüngliche (vereinfachte) Version könnte folgendermaßen aussehen:


 class UnlinkingFD { std::string path; public: int fd; UnlinkingFD(const std::string& p) : path(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD() { close(fd); unlink(path.c_str()); } UnlinkingFD(const UnlinkingFD&) = delete; }; 

Und das ist ein gutes, logisch solides Design. Es basiert auf RAII , um den Deskriptor automatisch freizugeben und den Link zu entfernen. Sie können ein großes Array solcher Objekte erstellen, mit ihnen arbeiten. Wenn das Array nicht mehr existiert, löschen die Objekte selbst alles, was im Prozess benötigt wurde.


Aber was ist mit der Leistung? Angenommen, fd sehr oft verwendet und path nur beim Löschen eines Objekts. Jetzt besteht das Array aus Objekten mit einer Größe von 40 Bytes, aber oft werden nur 4 Bytes verwendet. Dies bedeutet, dass mehr Fehler im Cache auftreten, da Sie 90% der Daten „überspringen“ müssen.


Eine der häufigsten Lösungen für dieses Problem ist der Übergang von einem Array von Strukturen zu einer Array-Struktur. Dies bietet die gewünschte Leistung, jedoch auf Kosten des Verzichts auf RAII. Gibt es eine Option, die die Vorteile beider Ansätze kombiniert?


Ein einfacher Kompromiss wäre, std::string Größe von 32 Bytes durch std::unique_ptr<std::string> zu ersetzen, dessen Größe nur 8 Bytes beträgt. Dadurch wird die Größe unseres Objekts von 40 Byte auf 16 Byte reduziert, was eine großartige Leistung ist. Diese Lösung verliert jedoch immer noch die Verwendung mehrerer Arrays.


OutOfLine ist ein Tool, mit dem Sie ohne Verwendung von RAII selten verwendete (kalte) Felder vollständig außerhalb des Objekts verschieben können. OutOfLine wird als CRTP- Basisklasse verwendet, daher muss das erste Argument für die Vorlage eine untergeordnete Klasse sein. Das zweite Argument ist der Typ selten verwendeter (kalter) Daten, der einem häufig verwendeten (Haupt-) Objekt zugeordnet ist.


 struct UnlinkingFD : private OutOfLine<UnlinkingFD, std::string> { int fd; UnlinkingFD(const std::string& p) : OutOfLine<UnlinkingFD, std::string>(p) { fd = open(p.c_str(), O_RDWR, 0); } ~UnlinkingFD(); UnlinkingFD(const UnlinkingFD&) = delete; }; 

Wie ist diese Klasse?


 template <class FastData, class ColdData> class OutOfLine { 

Die grundlegende Implementierungsidee besteht darin, einen globalen assoziativen Container zu verwenden, der Zeiger auf Hauptobjekte und Zeiger auf Objekte abbildet, die kalte Daten enthalten.


  inline static std::map<OutOfLine const*, std::unique_ptr<ColdData>> global_map_; 

OutOfLine kann mit jeder Art von kalten Daten verwendet werden, von denen eine Instanz automatisch erstellt und dem Hauptobjekt zugeordnet wird.


  template <class... TArgs> explicit OutOfLine(TArgs&&... args) { global_map_[this] = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } 

Das Entfernen des Hauptobjekts beinhaltet das automatische Entfernen des zugehörigen kalten Objekts:


  ~OutOfLine() { global_map_.erase(this); } 

Beim Verschieben (Konstruktor verschieben / Zuweisungsoperator verschieben) des Hauptobjekts wird das entsprechende kalte Objekt automatisch dem neuen Hauptnachfolgeobjekt zugeordnet. Daher sollten Sie nicht auf die kalten Daten eines verschobenen Objekts zugreifen.


  explicit OutOfLine(OutOfLine&& other) { *this = other; } OutOfLine& operator=(OutOfLine&& other) { global_map_[this] = std::move(global_map_[&other]); return *this; } 

Im obigen Implementierungsbeispiel wird OutOfLine der Einfachheit halber nicht kopierbar gemacht. Bei Bedarf lassen sich Kopiervorgänge einfach hinzufügen. Sie müssen lediglich eine Kopie eines kalten Objekts erstellen und verknüpfen.


 OutOfLine(OutOfLine const&) = delete; OutOfLine& operator=(OutOfLine const&) = delete; 

Damit dies wirklich nützlich ist, wäre es schön, Zugriff auf kalte Daten zu haben. Beim Erben von OutOfLine erhält OutOfLine Klasse die konstanten und nicht konstanten Methoden von cold() :


  ColdData& cold() noexcept { return *global_map_[this]; } ColdData const& cold() const noexcept { return *global_map_[this]; } 

Sie geben die entsprechende Art der Referenz auf kalte Daten zurück.


Das ist fast alles. Diese UnlinkingFD Option ist 4 Byte groß, bietet einen UnlinkingFD Zugriff auf das fd Feld und behält die Vorteile von RAII bei. Alle Arbeiten im Zusammenhang mit dem Lebenszyklus eines Objekts sind vollständig automatisiert. Wenn sich das häufig verwendete Hauptobjekt bewegt, werden selten verwendete kalte Daten mit verschoben. Wenn das Hauptobjekt gelöscht wird, wird auch das entsprechende kalte Objekt gelöscht.


Manchmal werden Ihre Daten jedoch verschworen, um Ihr Leben zu verkomplizieren - und Sie sind mit einer Situation konfrontiert, in der zuerst Basisdaten erstellt werden müssen. Sie werden beispielsweise benötigt, um kalte Daten zu erstellen. Es ist erforderlich, Objekte in umgekehrter Reihenfolge zu OutOfLine Angeboten von OutOfLine zu erstellen. In solchen Fällen ist eine Sicherung hilfreich, um die Reihenfolge der Initialisierung und De-Initialisierung zu steuern.


  struct TwoPhaseInit {}; OutOfLine(TwoPhaseInit){} template <class... TArgs> void init_cold_data(TArgs&&... args) { global_map_.find(this)->second = std::make_unique<ColdData>(std::forward<TArgs>(args)...); } void release_cold_data() { global_map_[this].reset(); } 

Dies ist ein weiterer OutOfLine Konstruktor, der in OutOfLine Klassen verwendet werden kann und ein Tag vom Typ TwoPhaseInit akzeptiert. Wenn Sie OutOfLine auf diese Weise erstellen, werden die kalten Daten nicht initialisiert und das Objekt bleibt zur Hälfte konstruiert. Um die zweiphasige Konstruktion init_cold_data , müssen Sie die Methode init_cold_data (indem Sie die Argumente übergeben, die zum Erstellen eines Objekts vom Typ ColdData ). Denken Sie daran, dass Sie .cold() für ein Objekt aufrufen .cold() dessen kalte Daten noch nicht initialisiert wurden. Analog können kalte Daten vorzeitig gelöscht werden, bevor der ~OutOfLine Destruktor ausgeführt wird, indem ~OutOfLine wird.


 }; // end of class OutOfLine 

Jetzt ist alles. Was geben uns diese 29 Codezeilen? Sie sind ein weiterer möglicher Kompromiss zwischen Leistung und Benutzerfreundlichkeit. In Fällen, in denen Sie ein Objekt haben, von dem einige Mitglieder viel häufiger als andere verwendet werden, kann OutOfLine als OutOfLine Methode zur Optimierung des Caches dienen, wodurch der Zugriff auf selten verwendete Daten erheblich verlangsamt wird.


Wir konnten diese Technik an mehreren Stellen anwenden - häufig müssen intensiv genutzte Arbeitsdaten durch zusätzliche Metadaten ergänzt werden, die am Ende der Arbeit in seltenen oder unerwarteten Situationen erforderlich sind. Ob es sich um Informationen über die Benutzer handelt, die die Verbindung hergestellt haben, über das Handelsterminal, von dem die Bestellung kam, oder über das Handle des Hardwarebeschleunigers, der Austauschdaten verarbeitet - OutOfLine hält den Cache sauber, wenn Sie sich im kritischen Teil der Berechnungen befinden (kritischer Pfad).


Ich habe einen Test vorbereitet , damit Sie den Unterschied sehen und bewerten können.


Das SkriptZeit (ns)
Kalte Daten im Hauptobjekt (Erstversion)34684547
Kalte Daten vollständig gelöscht (Best-Case-Szenario)2938327
OutOfLine verwenden2947645

Ich habe ungefähr OutOfLine Beschleunigung bei der Verwendung von OutOfLine . Dieser Test soll OutOfLine das Potenzial von OutOfLine , zeigt aber auch, wie stark die Cache-Optimierung einen erheblichen Einfluss auf die Leistung haben kann, genau wie OutOfLine es OutOfLine ermöglicht, diese Optimierung zu erhalten. Wenn Sie den Cache frei von selten verwendeten Daten halten, können Sie den Rest des Codes komplex, messbar und umfassend verbessern. Wie immer bei der Optimierung vertrauen Vertrauensmessungen mehr als Annahmen, dennoch hoffe ich, dass OutOfLine sich als nützliches Werkzeug in Ihrer Sammlung von Dienstprogrammen OutOfLine wird.


Anmerkung des Übersetzers


Der im Artikel bereitgestellte Code dient zur Veranschaulichung der Idee und ist nicht repräsentativ für den Produktionscode.

Source: https://habr.com/ru/post/de421475/


All Articles