Hallo,
Warum sollte lock nur im Ring0 funktionieren?
Als ich das lock-Präfix das letzte mal im Ring 3 benutzt hatte gabs ne Illegal-OpCode-Exception, kann aber auch an was anderes gelegen haben, da will ich mich jetzt nicht genau festlegen. Ich hatte halt diesen Zusammenhang irgendwie im Gedächtnis.
... aber ich sehe da kein Problem mit cmpxchg. Da die Cacheline ja nur in allen anderen CPUs geflusht werden muss, wenn wirklich geschriebene wird
Richtige Atomizität (nennt man das eigentlich so?) muss mit dem Lese-Zugriff beginnen. Jetzt überlege mal was passiert wenn 2 CPUs
gleichzeitig (das ist zwar nicht sehr wahrscheinlich aber doch möglich, ich denke als kritisches Zeit-Fenster können da etwa 4 CPU-Takte (vielleicht auch deutlich mehr) gelten) auf die Lock-Variable lesend zugreifen. Beide lesen das selbe ohne das einer vom anderen weiß. Wenn Du Dir die ARM-Implementierung ansiehst dann stellst Du fest das dort das Lock mit dem Lese-Befehl erstellt wird, die Lesebefehle werden spätestens im Speicher-Controller serialisiert (einer kommt eben als erster dran), wer dort verliert hat eben nicht den Lock. Beide Lese-Befehle liefern die selben Daten und beide CPUs werden gleichartig weitermachen aber nur bei einem wird der Schreib-Befehl zurückmelden das der Lock noch intakt war und beim anderen wird der ganze Code-Abschnitt, vom Lese-Befehl an, noch mal ausgeführt. Beim xchg-Befehl auf x86 wird die Atomizität im Speichercontroller damit erzeugt das dieser das Lesen der alten Daten und das Schreiben der neuen Daten als einen Zugriff atomar ausführt, ein gleichartiger Zugriff einer anderen CPU kommt eben danach und die bekommt beim Lesen auch sicher genau die Daten welche die erste CPU geschrieben hat. Beim Schreib-Part des cmpxchg-Befehls gibt es eben keine Rückmeldung das jemand anderes in der Zwischenzeit auch gelesen oder gar geschrieben hat. So könnten möglicherweise mehrere CPUs ermittelt haben das der Mutex/Spinlock/Semaphore vorher frei war und dann mehrere CPUs gleichzeitig drin sind. Idealer weise dürfte zwischen dem Lesen und dem zugehörigen Schreiben kein weiterer Zugriff, weder lesen noch schreiben, auf die Lock-Variable möglich sein, eben genau so wie es xchg immer macht. ARM hat das komplizierte mit einbeziehen des Speicher-Controllers, der ja gar nicht zur CPU gehört, nicht ohne Grund gemacht. Als kleine Optimierung könnte der Lesebefehl gleich zurückmelden ob der Lock überhaupt erstellt werden konnte, so das die CPU nicht vergeblich rechnen muss, so möchte ich es jedenfalls machen.
Der Vorteil ist halt, das cmpxchg nur schreibt wenn es wirklich durfte (d.h. wenn der Wert an der Stelle der erwartete ist).
Und genau da ist das Problem. Ich weiß nicht ob das lock-Präfix an diesem Verhalten nachbessert aber alles was ich bis jetzt über diesen Befehl gelesen hab lässt mich zweifeln. Möglicherweise sind meine Zweifel unbegründet, immerhin wird der cmpxchg-Befehl ja auch explizit für Semaphoren und Mutexes empfohlen und die Intel-Leute wissen hoffentlich was sie tun, aber ich persönlich bleibe (bei einer so elementaren/wichtigen Funktion) lieber auf der sicheren Seite.
Das erscheint mir zum einen korrekt zum anderen wegen weniger Cache Thrashing effizienter. Ich würde auch vermuten, dass das read in den Cache geht (was ja korrekt ist) und danach das write erst eine "richtige" Speicheroperation ist.
Wenn man davon ausgeht das Lock-Variablen prinzipiell nicht in den Cache dürfen, damit beim lesen auch immer der wirklich aktuelle Wert geliefert wird, dann ist xchg IMHO der effizientere Befehl.
Von den gcc buildins sind z.B. die ganzen __sync_fetch_and_operation und __sync_operation_and_fetch gar nicht für x86 implementierbar.
Das ist falsch, für add/sub wird die xadd Instruktion (mit lock) verwendet, was mir auf den ersten Blick völlig korrekt erscheint. Für die anderen wird cmpxchg verwendet, was auch sicherlich korrekt ist.
Okay, den Befehl kannte ich noch nicht. Das trifft aber nur auf __sync_fetch_and_add und __sync_fetch_and_sub zu. Die andere Variante __sync_add_and_fetch und __sync_sub_and_fetch gehen trotzdem nicht. Und alle anderen Rechenarten eben auch nicht. Oder gibt es da noch mehr Befehle die ich (noch) nicht kenne?
Wenn der Rückgabewert von den Operationen nicht verwendet wird, dann optimiert der gcc das zB zu einem add mit lock.
Das erwarte ich eigentlich auch.
Grüße
Erik