Ich habe eine x86-Assembly in Qts Quelle gesehen:
q_atomic_increment:
movl 4(%esp), %ecx
lock
incl (%ecx)
mov $0,%eax
setne %al
ret
.align 4,0x90
.type q_atomic_increment,@function
.size q_atomic_increment,.-q_atomic_increment
Durch Googeln wusste ich, dass
lock
Anweisungen dazu führen, dass die CPU den Bus sperrt, aber ich weiß nicht, wann die CPU den Bus freigibt ?Über den gesamten obigen Code verstehe ich nicht, wie dieser Code das implementiert
Add
?
lock
Präfix bewirkt und was ohne es passieren würde.Antworten:
LOCK
ist keine Anweisung selbst: Es ist ein Anweisungspräfix, das für die folgende Anweisung gilt. Diese Anweisung muss etwas sein , das macht eine Read-Modify-Write auf Speicher (INC
,XCHG
,CMPXCHG
etc.) --- in diesem Fall ist es das istincl (%ecx)
Befehl, derinc
das derungenl
Ong Wort an der Adresse in dem gehaltenenecx
Register.Das
LOCK
Präfix stellt sicher, dass die CPU für die Dauer des Vorgangs ausschließlich Eigentümer der entsprechenden Cache-Zeile ist, und bietet bestimmte zusätzliche Bestellgarantien. Dies kann durch Aktivieren einer Bussperre erreicht werden, aber die CPU wird dies nach Möglichkeit vermeiden. Wenn der Bus gesperrt ist, gilt dies nur für die Dauer des gesperrten Befehls.Dieser Code kopiert die Adresse der Variablen, die vom Stapel inkrementiert werden soll, in das
ecx
Register undlock incl (%ecx)
erhöht diese Variable dann atomar um 1. Die nächsten beiden Anweisungen setzen daseax
Register (das den Rückgabewert der Funktion enthält) auf 0, wenn die Der neue Wert der Variablen ist 0 und andernfalls 1. Die Operation ist ein Inkrement , kein Add (daher der Name).quelle
MOV
setzt allesEAX
auf Null.SETNE
ändert nur das Low-Byte. Ohne das würdenMOV
die 3 hohen Bytes vonEAX
zufällige Restwerte aus früheren Operationen enthalten, sodass der Rückgabewert falsch wäre.lock
ed-Befehl keine Cache-Zeile überschreiten, kann ein CPU-Kern diese Cache-Zeile nur intern sperren, anstatt alle Ladevorgänge / Speicher aller anderen Kerne zu blockieren. Siehe auch meine Antwort auf Kann num ++ für 'int num' atomar sein? Weitere Informationen dazu, wie dies funktioniert, damit es möglichen Beobachtern mithilfe des MESI-Cache-Kohärenz-Protokolls atomar erscheint .Was Sie möglicherweise nicht verstehen, ist, dass der zum Inkrementieren eines Werts erforderliche Mikrocode erfordert, dass wir zuerst den alten Wert einlesen.
Das Schlüsselwort Lock erzwingt, dass die tatsächlich auftretenden mehreren Mikrobefehle atomar zu funktionieren scheinen.
Wenn Sie zwei Threads hatten, die jeweils versuchten, dieselbe Variable zu inkrementieren, und beide gleichzeitig denselben ursprünglichen Wert lesen, erhöhen sie beide auf denselben Wert und schreiben beide denselben Wert aus.
Anstatt die Variable zweimal zu erhöhen, was die typische Erwartung ist, erhöhen Sie die Variable am Ende einmal.
Das Schlüsselwort lock verhindert dies.
quelle
LOCK
ist ein Befehlspräfix, daher gilt es nur für den folgenden Befehl. Die Quelle macht dies hier nicht sehr deutlich, aber der eigentliche Befehl istLOCK INC
. Der Bus wird also für das Inkrement gesperrt und dann entsperrtSie implementieren kein Add, sie implementieren ein Inkrement zusammen mit einer Rückgabeanzeige, wenn der alte Wert 0 war. Ein Add würde verwenden
LOCK XADD
(Windows InterlockedIncrement / Decrement werden jedoch auch mit implementiertLOCK XADD
).quelle
SETNE
die bedingten Flags von gesetztINC
.Beispiel für minimal ausführbare C ++ - Threads + LOCK-Inline-Assembly
main.cpp
#include <atomic> #include <cassert> #include <iostream> #include <thread> #include <vector> std::atomic_ulong my_atomic_ulong(0); unsigned long my_non_atomic_ulong = 0; unsigned long my_arch_atomic_ulong = 0; unsigned long my_arch_non_atomic_ulong = 0; size_t niters; void threadMain() { for (size_t i = 0; i < niters; ++i) { my_atomic_ulong++; my_non_atomic_ulong++; __asm__ __volatile__ ( "incq %0;" : "+m" (my_arch_non_atomic_ulong) : : ); __asm__ __volatile__ ( "lock;" "incq %0;" : "+m" (my_arch_atomic_ulong) : : ); } } int main(int argc, char **argv) { size_t nthreads; if (argc > 1) { nthreads = std::stoull(argv[1], NULL, 0); } else { nthreads = 2; } if (argc > 2) { niters = std::stoull(argv[2], NULL, 0); } else { niters = 10000; } std::vector<std::thread> threads(nthreads); for (size_t i = 0; i < nthreads; ++i) threads[i] = std::thread(threadMain); for (size_t i = 0; i < nthreads; ++i) threads[i].join(); assert(my_atomic_ulong.load() == nthreads * niters); assert(my_atomic_ulong == my_atomic_ulong.load()); std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl; assert(my_arch_atomic_ulong == nthreads * niters); std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl; }
GitHub stromaufwärts .
Kompilieren und ausführen:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp -pthread ./main.out 2 10000
Mögliche Ausgabe:
my_non_atomic_ulong 15264 my_arch_non_atomic_ulong 15267
Daraus sehen wir, dass das LOCK-Präfix die Addition atomar gemacht hat: Ohne sie haben wir Rennbedingungen für viele der Adds, und die Gesamtzahl am Ende ist geringer als die synchronisierten 20000.
Das LOCK-Präfix wird verwendet, um Folgendes zu implementieren:
std::atomic
: Was genau ist std :: atomic?atomic_int
: Wie starte ich Threads in einfachem C?Siehe auch: Wie sieht die Multicore-Assemblersprache aus?
Getestet in Ubuntu 19.04 amd64.
quelle
-O0
bringt es, das nichtatomare Inkrement mit einer vollen Barriere zu verwenden und zu fechten (lock inc
)? Um zu beweisen, dass es auch im besten Fall noch kaputt ist? Sie würden viel mehr verlorene Zählungen sehen, wenn Sie nicht gesperrteinc
aus dem Speicherpuffer weiterleiten lassen .-O0
: Ich hatte nicht viel darüber nachgedacht, standardmäßig für ein besseres Debugging, obwohl ich später bemerkt habe, dass es ein bisschen einfacher ist, das Verhalten in einem so einfachen Fall zu sehen, weil-O3
die Schleife auf ein einzelnes Add optimiert wird. "und das nichtatomare Inkrement mit einer vollen Barriere umzäunen": Beeinflusst LOCK auch die nichtatomaren Variablen im obigen Programm?lock inc
ist eine volle Barriere, wiemfence
. Sie haben keine 4 separaten Schleifen, Sie verschachteln Inkremente. Es macht das andereinc
Atom nicht , aber es zwingtinc
den Speicher, vor dem Laden des nächsten global sichtbar zu seininc
, also beeinflusst es ihn erheblich. Wenn Sie nicht-O3
aus der Schleife heben und tun möchten+= N
, können Sie verwendenvolatile
; Code-Gen einzuschränken, ohne irgendeine Art von Atomizität zu geben, ist das, wofür esvolatile
ist.