2008-09-16 7 views
14

Wann können 64-Bit-Schreibvorgänge garantiert atomar sein, wenn in C auf einer x86-basierten Intel-Plattform programmiert wird (insbesondere auf einem Intel-basierten Mac, auf dem MacOSX 10.4 mit dem Intel-Compiler läuft)? Zum Beispiel:Wie kann man garantieren, dass 64-Bit-Schreiboperationen atomar sind?

unsigned long long int y; 
y = 0xfedcba87654321ULL; 
/* ... a bunch of other time-consuming stuff happens... */ 
y = 0x12345678abcdefULL; 

Wenn ein anderer Thread, den Wert von y prüft, nachdem der erste Zuweisung y Ausführung beendet hat, möchte ich gewährleisten möchte, dass er sieht, entweder den Wert 0xfedcba87654321 oder den Wert 0x12345678abcdef, und nicht eine Mischung von ihnen. Ich möchte das ohne Sperren und wenn möglich ohne zusätzlichen Code machen. Ich hoffe, dass bei Verwendung eines 64-Bit-Compilers (des 64-Bit-Compilers von Intel) auf einem Betriebssystem, das 64-Bit-Code unterstützt (MacOSX 10.4), diese 64-Bit-Schreiboperationen atomar sind. Ist das immer wahr?

Antwort

2

GCC hat intrinsische Operationen für atomare Operationen; Ich vermute, du kannst das auch mit anderen Compilern machen. Verlassen Sie sich niemals auf den Compiler für atomare Operationen; Die Optimierung wird mit ziemlicher Sicherheit das Risiko eingehen, selbst atomare Operationen zu atomaren Operationen zu machen, es sei denn, Sie sagen dem Compiler explizit, dies nicht zu tun.

+1

Sie schlagen vor, GCC-Intrinsics zu verwenden, dann sagen Sie dem Compiler nicht zu vertrauen. Beziehen Sie sich auf etwas anderes als intrinsics, die dem Compiler nicht vertraut werden sollten? – Jeff

40

Ihre beste Wette ist es zu vermeiden, zu versuchen, Ihr eigenes System aus Primitiven zu erstellen, und stattdessen Sperren verwenden, es wirklich zeigt sich als Hot-Spot beim Profiling. (Wenn du denkst, du kannst schlau sein und Schlösser vermeiden, tu es nicht. Das tust du nicht. Das ist das allgemeine "du", das mich und alle anderen einschließt.) Du solltest mindestens einen Drehverschluss benutzen, siehe spinlock(3). Und was auch immer Sie tun, nicht versuchen, "Ihre eigenen" Sperren zu implementieren. Du wirst es falsch verstehen.

Letztendlich müssen Sie alle blockierenden oder atomaren Operationen Ihres Betriebssystems verwenden. Holen Sie sich diese Art von Dingen genau richtig in alle Fälle ist extrem schwierig. Oft kann es Wissen über Dinge wie die Errata für bestimmte Versionen eines bestimmten Prozessors beinhalten. ("Oh, Version 2.0 dieses Prozessors hat das Cache-Kohärenz-Snooping nicht zum richtigen Zeitpunkt durchgeführt, es ist in Version 2.0.1 behoben, aber auf 2.0 muss ein NOP eingefügt werden.") Einfach ein Schlüsselwort auf eine Variable klatschen in C ist fast immer nicht ausreichend.

Unter Mac OS X bedeutet dies, dass Sie die in atomic(3) aufgelisteten Funktionen verwenden müssen, um wirklich atomare Operationen für alle CPUs mit 32-Bit-, 64-Bit- und Zeigergrößen auszuführen. (Verwenden Sie Letzteres für alle unteilbaren Operationen auf Zeigern, so dass Sie automatisch 32/64-Bit-kompatibel sind.) Das ist egal, ob Sie atomare Vergleiche und Swaps, Inkremente/Dekremente, Spin Locking oder Stack/Queue machen wollen Management. Zum Glück ist die spinlock(3), atomic(3) und barrier(3) Funktionen sollen alle Arbeiten korrekt auf allen CPUs, die processor manuals,

+7

Vielen Dank für solch einen warmen und unscharfen Ort, um zukünftige 'praktische' kostenlose Evangelikale zu senden :) + 10 wenn ich könnte. –

9

Nach Kapitel 7 von Part 3A - System Programming Guide von Intel von Mac OS X unterstützt werden fach-Wort-Zugriffe werden atomar ausgeführt werden, wenn auf einem 64 ausgerichtet werden -Bit-Grenze, auf einem Pentium oder neuer, und nicht ausgerichtet (wenn immer noch in einer Cache-Zeile) auf einem P6 oder neuer. Sie sollten volatile verwenden, um sicherzustellen, dass der Compiler nicht versucht, den Schreibvorgang in einer Variablen zwischenzuspeichern, und Sie müssen möglicherweise eine Memory Fence-Routine verwenden, um sicherzustellen, dass der Schreibvorgang in der richtigen Reihenfolge erfolgt.

Wenn Sie den Wert auf einem vorhandenen Wert geschrieben Basis benötigen, können Sie Ihr Betriebssystem Verschlungene Funktionen verwenden sollten (zum Beispiel Windows verfügt über InterlockedIncrement64).

+2

Um noch genauer zu sein, ist es in §8.8.1 auf Seite 325 angegeben. –

+0

Wenn Sie die richtige Schnittstelle zu Atomics richtig verwenden, brauchen Sie nicht "flüchtig". – Jeff

2

Wenn Sie so etwas wie dies für interthread oder Interprozess-Kommunikation tun wollen, dann müssen Sie mehr als nur eine atomare Lese-/Schreib-Garantie. In Ihrem Beispiel scheint es, dass die geschriebenen Werte anzeigen sollen, dass ein Teil der Arbeit gerade ausgeführt wird und/oder abgeschlossen wurde. Sie müssen mehrere Dinge tun, von denen nicht alle portierbar sind, um sicherzustellen, dass der Compiler die Dinge in der Reihenfolge ausgeführt hat, in der sie ausgeführt werden sollen (das flüchtige Schlüsselwort kann bis zu einem gewissen Grad helfen) und dass der Speicher konsistent ist. Moderne Prozessoren und Caches können die Arbeit außer Betrieb ausführen, ohne dass der Compiler dies bemerkt. Daher benötigen Sie wirklich etwas Plattformunterstützung (dh Sperren oder plattformspezifische verriegelte APIs), um das zu tun, was Sie zu tun scheinen.

"Speicherzaun" oder "Speicherbarriere" sind Begriffe, die Sie erforschen möchten.

+0

'mfence' ist Overkill für eine Producer-Consumer-Warteschlange. Sie brauchen nur "sfence" auf der Produzentenseite und "lfence" auf der Konsumentenseite. Es gibt keinen Artikel mit dem Titel "Speicherbarrieren als schädlich", aber es sollte sein :-) – Jeff

10

Auf Intel MacOSX können Sie die integrierten atomaren Operationen des Systems verwenden. Es gibt kein bereitgestelltes atomisches Get oder Set für 32 oder 64 Bit Ganzzahlen, aber Sie können das aus dem bereitgestellten CompareAndSwap erstellen. Möglicherweise möchten Sie die XCode-Dokumentation für die verschiedenen OSAtomic-Funktionen durchsuchen. Ich habe die folgende 64-Bit-Version geschrieben. Die 32-Bit-Version kann mit ähnlich benannten Funktionen ausgeführt werden.

#include <libkern/OSAtomic.h> 
// bool OSAtomicCompareAndSwap64Barrier(int64_t oldValue, int64_t newValue, int64_t *theValue); 

void AtomicSet(uint64_t *target, uint64_t new_value) 
{ 
    while (true) 
    { 
     uint64_t old_value = *target; 
     if (OSAtomicCompareAndSwap64Barrier(old_value, new_value, target)) return; 
    } 
} 

uint64_t AtomicGet(uint64_t *target) 
{ 
    while (true) 
    { 
     int64 value = *target; 
     if (OSAtomicCompareAndSwap64Barrier(value, value, target)) return value; 
    } 
} 

Hinweis, dass Apples OSAtomicCompareAndSwap Funktionen atomar die Operation ausführen:

Wir verwenden diese in dem obigen Beispiel eine Set-Methode zu erstellen, indem man zuerst den alten Wert greifen, dann Speicher das Ziel zu tauschen versucht, die Wert. Wenn der Swap erfolgreich ist, zeigt dies an, dass der Wert des Speichers zum Zeitpunkt des Swap immer noch der alte Wert ist, und während des Swaps (der wiederum atomar ist) erhält er den neuen Wert, also sind wir fertig. Wenn dies nicht gelingt, hat sich ein anderer Thread durch eine Änderung des Werts dazwischen gestört, als wir ihn ergriffen haben und versucht haben, ihn zurückzusetzen. Wenn das passiert, können wir einfach Schleife und versuchen Sie es erneut mit nur minimaler Strafe.

Die Idee hinter der Get-Methode ist, dass wir zuerst den Wert greifen können (was, kann oder kann nicht der tatsächliche Wert, wenn ein anderer Thread stört). Wir können dann versuchen, den Wert mit sich selbst zu vertauschen, einfach um zu überprüfen, ob der anfängliche Zugriff gleich dem atomaren Wert war.

Ich habe dies nicht gegen meinen Compiler überprüft, also bitte alle Tippfehler entschuldigen.

Sie ausdrücklich erwähnt, OSX, aber Sie benötigen, falls auf anderen Plattformen zu arbeiten, hat Windows-eine Reihe von Interlocked * Funktionen, und Sie können sie in der MSDN-Dokumentation suchen. Einige von ihnen funktionieren unter Windows 2000 Pro und höher, und einige (insbesondere einige der 64-Bit-Funktionen) sind neu in Vista. Auf anderen Plattformen verfügen GCC-Versionen 4.1 und höher über verschiedene __sync * -Funktionen, z. B. __sync_fetch_and_add(). Für andere Systeme müssen Sie möglicherweise Assembly verwenden, und Sie finden einige Implementierungen im SVN-Browser für das HaikuOS-Projekt in src/system/libroot/os/arch.

+0

Zum Lesen können Sie viel einfacheren Ansatz 'OSAtomicAdd64Barrier (0, Ziel)' 'verwenden, es fügt 0 automatisch auf die Variable von Ziel und Renditen das Ergebnis der Addition, in diesem Fall * Ziel selbst –

6

auf x86-, dem schnellsten Weg, um atomar einen ausgerichteten 64-Bit-Wert zu schreiben ist FISTP zu verwenden. Für nicht ausgerichtete Werte müssen Sie ein CAS2 (_InterlockedExchange64) verwenden. Die CAS2-Operation ist jedoch aufgrund von BUSLOCK ziemlich langsam, daher kann es häufig schneller sein, die Ausrichtung zu überprüfen und die FISTP-Version für ausgerichtete Adressen auszuführen. In der Tat implementiert das Intel Threaded building Blocks Atomic 64-Bit-Schreibvorgänge.

+1

Per https://software.intel.com/en-us/articles/implementing-scalable-atomic-locks-for-multi-core-intel-em64t-and-ia32 -Architektur, CMPXCHG hat keine Bussperre seit dem Intel Pentium Pro Prozessor impliziert. – Jeff

1

Die neueste Version von ISO C (C11) definiert eine Reihe von atomaren Operationen, einschließlich atomic_store(_explicit). Siehe z.B. this page für weitere Informationen.

Die am zweitmeisten tragbare Implementierung von Atomics sind die GCC-Intrinsics, die bereits erwähnt wurden. Ich finde, dass sie vollständig von GCC-, Clang-, Intel- und IBM-Compilern unterstützt werden und - wie ich das letzte Mal überprüft habe - teilweise von den Cray-Compilern unterstützt werden.

Ein klarer Vorteil von C11 Atomics - zusätzlich zu dem ganzen ISO-Standard - ist, dass sie eine präzisere Speicherkonsistenz unterstützen. Die GKK-Atome implizieren, soweit ich weiß, eine vollständige Speicherbarriere.