Was bedeutet die Anweisung "Sperren" in der x86-Assembly?


69

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
  1. Durch Googeln wusste ich, dass lockAnweisungen dazu führen, dass die CPU den Bus sperrt, aber ich weiß nicht, wann die CPU den Bus freigibt ?

  2. Über den gesamten obigen Code verstehe ich nicht, wie dieser Code das implementiert Add?



1
verwandt: meine Antwort auf Kann num ++ für 'int num' atomar sein? erklärt die Atomizität auf x86 und was genau das lockPräfix bewirkt und was ohne es passieren würde.
Peter Cordes

Antworten:


103
  1. LOCKist 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, CMPXCHGetc.) --- in diesem Fall ist es das ist incl (%ecx)Befehl, der incdas derungen lOng Wort an der Adresse in dem gehaltenen ecxRegister.

    Das LOCKPrä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.

  2. Dieser Code kopiert die Adresse der Variablen, die vom Stapel inkrementiert werden soll, in das ecxRegister und lock incl (%ecx)erhöht diese Variable dann atomar um 1. Die nächsten beiden Anweisungen setzen das eaxRegister (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).


Die Anweisung "mov $ 0,% eax" scheint also überflüssig zu sein?
Gemfield

4
@gemfield: Nein, das MOVsetzt alles EAXauf Null. SETNEändert nur das Low-Byte. Ohne das würden MOVdie 3 hohen Bytes von EAXzufällige Restwerte aus früheren Operationen enthalten, sodass der Rückgabewert falsch wäre.
Anthony Williams

1
In einem der russischen Bücher "Assembler für DOS, Windows и Linux, 2000" erwähnte der Autor von Sergei Zukkov Folgendes zu diesem Präfix: "Während der gesamten Befehlszeit, die mit einem solchen Präfix versehen ist, wird der Datenbus angehalten. Wenn ein System über einen anderen Prozessor verfügt, kann es erst am Ende des Befehls mit dem Präfix LOCK auf den Speicher zugreifen. Der XCHG-Befehl wird automatisch immer mit der Speicherzugriffssperre ausgeführt, auch wenn das Präfix LOCK nicht angegeben ist. Dieses Präfix kann verwendet werden nur mit den Befehlen ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD und XCHG. "
Bruziuz

3
@bruziuz: Moderne CPUs sind viel effizienter: Wenn die Daten für einen locked-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 .
Peter Cordes

Vielen Dank! Cool! :)
Bruziuz

13

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.


11

Von Google wusste ich, dass eine Sperranweisung dazu führt, dass die CPU den Bus sperrt, aber ich weiß nicht, wann die CPU den Bus freigibt?

LOCKist ein Befehlspräfix, daher gilt es nur für den folgenden Befehl. Die Quelle macht dies hier nicht sehr deutlich, aber der eigentliche Befehl ist LOCK INC. Der Bus wird also für das Inkrement gesperrt und dann entsperrt

Über den gesamten obigen Code verstehe ich nicht, wie dieser Code das Hinzufügen implementiert hat?

Sie 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 implementiert LOCK XADD).


Vielen Dank! Welches Register speichert dann den Wert des Rückgabewerts der Funktion (q_atomic_increment)?
Gemfield

1
Rückgabewerte werden in% eax
nos

Der Code: "return q_atomic_increment (& _ q_value)! = 0" soll also testen, ob% eax ungleich Null ist?
Gemfield

@gemfield: Es ist Null, dann wird das LSB über SETNEdie bedingten Flags von gesetzt INC.
Necrolis

Ist es, ob der alte Wert 0 war oder nicht, der in% eax zurückgegeben wird (wie die Antwort derzeit sagt), oder der neue Wert?
Tom

2

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:

Siehe auch: Wie sieht die Multicore-Assemblersprache aus?

Getestet in Ubuntu 19.04 amd64.


Was -O0bringt 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 gesperrte incaus dem Speicherpuffer weiterleiten lassen .
Peter Cordes

@PeterCordes -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 -O3die 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?
Ciro Santilli 法轮功 冠状 病 六四 事件 17

1
lock incist eine volle Barriere, wie mfence. Sie haben keine 4 separaten Schleifen, Sie verschachteln Inkremente. Es macht das andere inc Atom nicht , aber es zwingt incden Speicher, vor dem Laden des nächsten global sichtbar zu sein inc, also beeinflusst es ihn erheblich. Wenn Sie nicht -O3aus der Schleife heben und tun möchten += N, können Sie verwenden volatile; Code-Gen einzuschränken, ohne irgendeine Art von Atomizität zu geben, ist das, wofür es volatileist.
Peter Cordes
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.