Die übliche Implementierung ist eine Hash-Tabelle mit Mutexen (oder auch nur einfachen Spinlocks ohne Rückgriff auf OS-unterstütztes Sleep / Wakeup), wobei die Adresse des atomaren Objekts als Schlüssel verwendet wird . Die Hash-Funktion ist möglicherweise so einfach wie die Verwendung der niedrigen Bits der Adresse als Index für ein Array mit einer Potenz von 2, aber die Antwort von @ Frank zeigt, dass die std :: atomic-Implementierung von LLVM XOR in einigen höheren Bits ausführt, sodass Sie dies nicht tun. t erhält automatisch ein Aliasing, wenn Objekte durch eine große Zweierpotenz getrennt sind (was häufiger vorkommt als bei jeder anderen zufälligen Anordnung).
Ich denke (aber ich bin nicht sicher), dass g ++ und clang ++ ABI-kompatibel sind; Das heißt, sie verwenden dieselbe Hash-Funktion und Tabelle, sodass sie sich darauf einigen, welche Sperre den Zugriff auf welches Objekt serialisiert. Das Sperren erfolgt jedoch vollständig in libatomic
. Wenn Sie also dynamisch verknüpfen, verwendet der libatomic
gesamte Code innerhalb desselben Programms, das aufgerufen __atomic_store_16
wird, dieselbe Implementierung. clang ++ und g ++ sind sich definitiv einig, welche Funktionsnamen aufgerufen werden sollen, und das ist genug. (Beachten Sie jedoch, dass nur sperrfreie atomare Objekte im gemeinsam genutzten Speicher zwischen verschiedenen Prozessen funktionieren: Jeder Prozess verfügt über eine eigene Hash-Tabelle mit Sperren . Sperrenfreie Objekte sollen (und tatsächlich) nur im gemeinsam genutzten Speicher auf einer normalen CPU arbeiten Architekturen, auch wenn die Region unterschiedlichen Adressen zugeordnet ist.)
Hash-Kollisionen bedeuten, dass zwei atomare Objekte möglicherweise dieselbe Sperre verwenden. Dies ist kein Korrektheitsproblem, aber es könnte ein Leistungsproblem sein : Anstatt zwei Threadpaare, die separat um zwei verschiedene Objekte miteinander konkurrieren, könnten alle vier Threads um den Zugriff auf eines der beiden Objekte kämpfen. Vermutlich ist das ungewöhnlich, und normalerweise möchten Sie, dass Ihre atomaren Objekte auf den Plattformen, die Sie interessieren, frei von Sperren sind. Aber die meiste Zeit hat man nicht wirklich Pech und es ist im Grunde in Ordnung.
Deadlocks sind nicht möglich, da es keine std::atomic
Funktionen gibt, die versuchen, zwei Objekte gleichzeitig zu sperren. Der Bibliothekscode, der die Sperre übernimmt, versucht also niemals, eine andere Sperre zu aktivieren, während er eine dieser Sperren hält. Zusätzliche Konflikte / Serialisierungen sind kein Korrektheitsproblem, sondern nur die Leistung.
x86-64 16-Byte-Objekte mit GCC vs. MSVC :
Als Hack können Compiler das lock cmpxchg16b
Laden / Speichern von 16-Byte-Atomen sowie tatsächliche Lese-, Änderungs- und Schreibvorgänge implementieren.
Dies ist besser als das Sperren, weist jedoch im Vergleich zu 8-Byte-Atomobjekten eine schlechte Leistung auf (z. B. konkurrieren reine Lasten mit anderen Lasten). Dies ist der einzige dokumentierte sichere Weg, um mit 16 Bytes 1 atomar etwas zu tun .
AFAIK, MSVC verwendet niemals lock cmpxchg16b
für 16-Byte-Objekte und sie sind im Grunde die gleichen wie ein 24- oder 32-Byte-Objekt.
gcc6 und früher lock cmpxchg16b
beim Kompilieren -mcx16
inline (cmpxchg16b ist leider keine Basis für x86-64; AMD K8-CPUs der ersten Generation fehlen es.)
gcc7 hat beschlossen, libatomic
16-Byte-Objekte immer aufzurufen und niemals als sperrenfrei zu melden, obwohl libatomische Funktionen lock cmpxchg16b
auf Computern, auf denen die Anweisung verfügbar ist, weiterhin verwendet werden. Siehe is_lock_free () hat nach dem Upgrade auf MacPorts gcc 7.3 false zurückgegeben . Die gcc-Mailinglistennachricht, die diese Änderung erklärt, ist hier .
Sie können einen Union-Hack verwenden, um einen relativ billigen ABA-Zeiger + Zähler auf x86-64 mit gcc / clang zu erhalten: Wie kann ich einen ABA-Zähler mit c ++ 11 CAS implementieren? . lock cmpxchg16b
für Aktualisierungen von Zeiger und Zähler, aber einfaches mov
Laden nur des Zeigers. Dies funktioniert jedoch nur, wenn das 16-Byte-Objekt tatsächlich sperrenfrei lock cmpxchg16b
ist.
Fußnote 1 : Das movdqa
Laden / Speichern von 16 Byte ist in der Praxis auf einigen (aber nicht allen) x86-Mikroarchitekturen atomar , und es gibt keine zuverlässige oder dokumentierte Methode, um zu erkennen, wann es verwendbar ist. Siehe Warum ist die Ganzzahlzuweisung für eine natürlich ausgerichtete Variable auf x86 atomar? und SSE-Anweisungen: Welche CPUs können atomare 16B-Speicheroperationen ausführen? Ein Beispiel, bei dem K10 Opteron mit HyperTransport nur zwischen Sockets an 8B-Grenzen reißt.
Compiler-Writer müssen also auf Nummer sicher gehen und können movdqa
die Art und Weise, wie sie SSE2 movq
für das Laden / Speichern von 8-Byte-Atomen in 32-Bit-Code verwenden, nicht verwenden. Es wäre großartig, wenn CPU-Anbieter einige Garantien für einige Mikroarchitekturen dokumentieren oder CPUID-Feature-Bits für das Laden / Speichern von atomaren Vektoren mit 16, 32 und 64 Byte (mit SSE, AVX und AVX512) hinzufügen könnten. Vielleicht könnten die Mobo-Anbieter die Firmware auf funky Maschinen mit vielen Sockeln deaktivieren, die spezielle Kohärenz-Klebechips verwenden, die nicht ganze Cache-Zeilen atomar übertragen.