Что означает команда "lock" в сборке x86?

Я видел некоторую сборку x86 в источнике Qt:

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
  • Из Googling я знал, что команда lock заставит CPU блокировать шину, но я не знаю, когда процессор освобождает шину?

  • Что касается всего кода выше, я не понимаю, как этот код реализует Add?

Ответ 1

  • LOCK не является самой инструкцией: это префикс команды, который применяется к следующей инструкции. Эта инструкция должна быть чем-то, что делает чтение-модификацию-запись в памяти (INC, XCHG, CMPXCHG и т.д.) --- в этом случае это инструкция incl (%ecx), которая INC t26 > ong ​​по адресу, хранящемуся в регистре ecx.

    Префикс LOCK гарантирует, что ЦП имеет эксклюзивное право собственности на соответствующую строку кэша на время действия и предоставляет определенные дополнительные гарантии заказа. Это может быть достигнуто путем утверждения блокировки шины, но CPU, по возможности, избежит этого. Если шина заблокирована, то это будет только на время заблокированной инструкции.

  • Этот код копирует адрес переменной, которая должна быть увеличена с стека в регистр ecx, а затем lock incl (%ecx) для атомарного увеличения этой переменной на 1. Следующие две команды устанавливают eax register (который возвращает возвращаемое значение от функции) до 0, если новое значение переменной равно 0 и 1 в противном случае. Операция - это приращение, а не добавление (отсюда и название).

Ответ 2

То, что вы, возможно, не понимаете, это то, что микрокод, необходимый для увеличения значения, требует, чтобы мы сначала прочитали старое значение.

Ключевое слово Lock заставляет несколько микрокоманд, которые на самом деле возникают, чтобы работать в атомарном режиме.

Если у вас было 2 потока, каждый из которых пытался увеличить одну и ту же переменную, и оба они одновременно считывали одно и то же исходное значение, то оба они увеличиваются до одного значения, и оба они выписывают одно и то же значение.

Вместо того, чтобы переменная увеличивалась дважды, что является типичным ожиданием, вы в конечном итоге увеличиваете значение переменной один раз.

Ключевое слово блокировки предотвращает это.

Ответ 3

Из google, я знал, что команда lock заставит CPU блокировать шину, но я не знаете, когда cpu освободит автобус?

LOCK - это префикс команды, поэтому он применим только к следующей инструкции, источник здесь не очень ясен, но реальная инструкция LOCK INC. Таким образом, шина заблокирована для приращения, затем разблокирована

Что касается всего кода выше, я не понимаю, как этот код реализована функция Add?

Они не реализуют Add, они реализуют приращение вместе с индикацией возврата, если старое значение равно 0. В дополнение будет использовать LOCK XADD (однако, окна InterlockedIncrement/Decrement также реализуются с помощью LOCK XADD)..

Ответ 4

Минимальная работоспособность C++ резьбы + пример встроенной сборки LOCK

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 вверх по течению.

Скомпилируйте и запустите:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp -pthread
./main.out 2 10000

Возможный вывод:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

Из этого мы видим, что префикс LOCK сделал добавление атомарным: без него у нас есть условия гонки для многих добавлений, и общее количество в конце меньше, чем синхронизированные 20000.

Смотрите также: Как выглядит многоядерный язык ассемблера?

Проверено в Ubuntu 19.04 amd64.