Hallo,
Außerdem ist die Semantik von cmpxchg eine andere als alles, was du mit xchg + weitere Befehle bauen kannst. Also es lassen sich zum Beispiel Algorithmen implementieren, die mit xchg nicht machbar sind, beziehungsweise bessere "Performance" (z.B. wait-freedom) haben.
Das mit cmpxchg bestimmte Dinge "einfacher" (zumindest für den Assembler-Programmierer) gehen steht außer Frage. xchg ist nur für einfache Locks geeignet so wie eben ein klassischer Test-and-Set-Befehl, aber diese Aufgabe beherrscht xchg mit maximaler Effizienz.
Hier im Thread wurde das geringere Cache Thrashing als Vorteil genannt.
Okay, dann wollen wir uns das mal an einem Beispiel ansehen:
Ich bleibe bewusst bei einem einfachen Lock (0x00 für frei und 0x01 für belegt) und beziehe mich für die xchg-Variante auf meinen 4-Zeiler vom 17.03.
Folgende Umgebung: 8 CPUs (Anzahl der Kerne pro CPU lassen wir der Einfachheit halber mal bei 1), jede mit eigenem Speicher in einem modernem ccNUMA-CPU-Netzwerk (ob per HyperTransport oder QPI ist auch egal, ich denke die Unterschiede sind gering, auch wenn AMD und Intel das natürlich ganz anders darstellen).
A--B--C--D
| | | |
E--F--G--HDie CPUs A und H wollen (fast) zeitgleich einen Lock betreten dessen Lock-Variable (einfaches Byte) im Speicher der CPU G liegt. Der Lock ist momentan frei, also 0x00. Falls die Cache-Line mit der Lock-Variable in einem Cache liegt (was nicht in jedem meiner Szenarien so ist) dann modifiziert in CPU D.
- xchg-Szenario:
Das Lock-Byte liegt in keinem Cache weil die betreffende Page als non-cachable markiert ist. Die CPUs A und H schicken jeweils eine Locked-Exchange-Anfrage an CPU G, die enthaltenden Schreibdaten sind jeweils 0x01. Eine von beiden Anfrage erreicht die CPU G als erstes und der Memory-Controller antwortet mit einer 0x00 für den Gewinner und mit 0x01 für den Verlierer. Die Gewinner-CPU weiß das der Lock vorher frei war und fühlt sich zu recht als neuer Eigentümer (in der Lock-Variable steht nun sicher 0x01) und die Verlierer-CPU weiß das der Lock bereits belegt war und versucht es noch mal. Ich denke das Szenario ist recht klar und mann kann sich ziemlich genau ausrechnen wie lange der xchg-Befehl dauert: 2 * Transport-Zeit der Messages und ein mal DRAM-Zugriff (fürs Lesen, das Schreiben auf eine bereits aktive DRAM-Zeile können wir jetzt mal vernachlässigen), insgesamt ist das einem normalem Lesezugriff sehr ähnlich. Selbst wenn der Speicher-Controller die Jobs umsortieren sollte ist das kein Problem, dann gewinnt eben die andere CPU. Ein Cache-Kohärenz-Protokoll ist nicht erforderlich weil ja non-cachable. Der Zeitbedarf für vorher frei und vorher belegt ist logischerweise immer gleich.
Falls die Lock-Variable in einem der Caches liegt, was bei einer als cachable gekennzeichneten Page ja sein kann, dann muss vorher natürlich eine Write-Backe-Message an alle CPUs übermittelt werden. Wie lange das dauert kann ich nicht genau sagen aber es ist möglicherweise eventuell länger als der xchg-Befehl an sich braucht. Die CPU, welche den xchg-Befehl ausführt, müsste entweder pauschal eine bestimmte Zeit warten oder von der entferntesten CPU eine Rückmeldung bekommen. Alternativ, zur Beschleunigung, könnte man die Locked-Exchange-Anfrage einfach hinter der Write-Back-Message hinterher schicken und alle CPUs müssten dafür sorgen das sich die Messages nicht überholen, das heißt das bei einer CPU wo die betreffende Cache-Line modifiziert ist diese zuerst geschrieben werden muss und dann die 2 Messages erst weitergeleitet werden dürften. Falls die Locked-Exchange-Anfrage (welche natürlich nicht vervielfältigt werden darf, im Gegensatz zur Write-Back-Message welche ja an alle CPUs gebroadcastet werden muss) von CPU A über CPU E geleitet wird und die modifizierte Cache-Line in CPU D ist dann wird das zwar etwas kompliziert aber ist bestimmt noch möglich, CPU G müsste die Locked-Exchange-Anfrage so lange verzögern bis alle Write-Backes durchgeführt wurden. Den Zeitbedarf für dieses Cache-Kohärenz-Protokoll kann ich nur schwer abschätzen, mit allen Optimierungen sollte es etwas weniger sein als der xchg-Befehl alleine benötigt.
- das ARM-Szenario mit ldrex und strex:
Auch hier ist die betreffende Page als non-cachable markiert. Der ldrex-Befehl holt aus dem Speicher das betreffende Byte und richtet im Memory-Controller der CPU G einen Hardware-Lock ein. Die CPU entscheidet dann anhand des gelesenen Wertes ob der (SW-verwaltete) Lock noch frei war und schreibt immer 0x01 per strex-Befehl zurück in den Speicher. Der strex-Befehl hebt auch wieder den Hardware-Lock im Memory-Controller der CPU G auf und meldet an die CPU (A oder H) den Erfolg zurück. Falls eine zweite CPU (H oder A) kurz nach dem Lesezugriff der ersten CPU auch dran kommt dann wird für den ldrex-Befehl dieser CPU kein Hardware-Lock erstellt und nach dem Schreibzugriff merkt diese das sie verloren hat (hier wäre es natürlich geschickter wenn bereits der ldrex-Befehl einen Fehler melden könnte) und wiederholt den gesamten Vorgang. Der Zeitbedarf ist ziemlich genau das doppelte der xchg-Variante, sind ja 2 komplette Zugriffe. Natürlich lasen sich zwischen den beiden Befehlen auch komplexere Dinge erledigen, mindestens alles was man mit cmpxchg auch machen kann. Der Mehrbedarf dürfte in Anbetracht heutiger CPU-Performance vernachlässigbar sein. Der Zeitbedarf für vorher frei und vorher belegt ist gleich weil die CPU leider erst nach dem abschließenden Schreibzugriff merkt ob sie erfolgreich war oder nicht (da ließe sich aber noch optimieren).
Falls die betreffende Cache-Line doch gecached ist gelten die selben Dinge wie bei xchg, nur das der zusätzliche Zeitaufwand prozentual gesehen geringer ist. Das Cache-Kohärenz-Protokoll muss vor/mit dem ldrex-Befehl erfolgen.
- cmpxchg-Szenario :
....... wer mag sich daran versuchen und zeigen wo cmpxchg mit lock-Präfix (und ohne Cache-Trashing) schneller ist? Ich lerne gerne noch dazu.
Ich hab mal etwas gegrübelt und mir ist nichts eingefallen das cmpxchg wenigstens schneller als eine optimierte ARM-Variante machen würde, an die xchg-Variante dürfte cmpxchg erst recht nicht ran kommen, selbst dann nicht wenn man das Cache-Kohärenz-Protokoll in der ungünstigsten Variante annimmt.
Grüße
Erik