Dieser Blogartikel stellt ein Experiment dar: Vor ein paar Tagen erhielt ich von Oliver Schädlich die unten stehende E-Mail. Wenn dieser Artikel, wie ich hoffe, eine Diskussion entfacht, fasse ich sie gerne in meinem nächsten Artikel zusammen. Schreibt dazu bitte eine E-Mail an Rainer Grimm.
Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.
Die Mail von Oliver Schädlich:
"Ich hab gerade Ihren aktuellen Artikel über Hazard Pointer mit der Ausschau auf RCU bei Heise gelesen. Angeregt durch Vorträge auf YouTube zu lock-freien Varianten von atomic<shared_ptr<>> habe ich mir mal gedacht: Das muss man doch alles nicht so kompliziert haben. Im Prinzip könnte man so was wie einen atomic<shared_ptr<>> einfach mit Lock implementieren und beim Update des shared_ptr<> vom zentralen atomic<shared_ptr<>> wird dann erstmal, ohne zu locken, ein Vergleich des Atomics mit dem shared_ptr durchgeführt.
In der Regel ist das ja so, dass bei RCU-artigen Patterns mit einem zentralen atomic<shared_ptr<>> dieser relativ selten aktualisiert wird, man sich aber sehr häufig ein Update des aktuellen Werts holt. Im Regelfall, dass beide Zeiger auf dasselbe zeigen, lockt man einfach nicht und man hat ein superschnelles Update, weil alle beteiligten Cachezeilen weiterhin auf shared stehen.
Ich habe das mal rudimentär mit VS 2022 implementiert und das Update des shared_obj<T> vom zentralen tshared_obj<> kostet auf meiner Zen4-CPU ca. 1,5 Nanosekunden. Die Lösung ist so simpel, dass ich mich gefragt habe, warum da noch keiner drauf gekommen ist.
Im Prinzip könnte man dem shared_ptr<> von C++ einen Zuweisungsoperator geben, der einen atomic<shared_ptr<>> nimmt, der das Gleiche tut. Aktuell läuft das ja so, dass der atomic<shared_ptr<>> dann gegen shared_ptr<> castet und ein relativ teures Kopieren anfällt. Die libstdc++ und MSVC arbeiten an der Stelle aktuell mit einem Lock, was weiterhin kein Problem wäre, wenn man für den besagten Sonderfall wie beschrieben vorgeht. Vielleicht können Sie die Idee mal an die entsprechende Stelle weitertragen, dass sich so was in irgendeinem der nächsten Standards wiederfindet.
Hier ist der Code, den Oliver Schädlich mir geschickt hat. Er ist in C++20 geschrieben und verwendet die Standardbibliothek. Die Dateien sind:
- futex.cpp
- main.cpp
- cl_size.h
- futex.h
- shared_obj.h
Am interessantesten ist die Datei shared_obj.h und darin die folgende Funktion:
template<typename T> shared_obj<T> &shared_obj<T>::operator =( tshared_obj<T> const &tso ) noexcept { using namespace std; ctrl_t *ctrl = tso.m_ctrl.load( memory_order_relaxed ); if( ctrl == m_ctrl ) return *this; if( m_ctrl ) { if( m_ctrl->decr() == 1 ) delete m_ctrl; m_ctrl = nullptr; } lock_guard<typename tshared_obj<T>::mutex_t> lock( tso.m_mtx ); ctrl = tso.m_ctrl.load( memory_order_relaxed ); m_ctrl = ctrl; ctrl->incr(); return *this; }Wenn ich das erste if mit return *this wegnehme, ist der Code gleich ein paar tausendmal langsamer, ungefähr so langsam wie das Update eines shared_ptr<> von einem atomic<shared_ptr<>>.
Wie gehts weiter?
RCU steht für Read Copy Update, eine Synchronisationstechnik für fast ausschließlich schreibgeschützte Datenstrukturen, die von Paul McKenney entwickelt wurde und seit 2002 im Linux-Kernel verwendet wird. RCU wird derzeit im Zusammenhang mit Hazard Pointers erwähnt.
(who)
>" – ein Experiment" data-teaser-tracking-content="gift_curtain" google-curtain >