Что означает инструкция "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
  1. От Google, я знал, lock инструкция заставит процессор блокировать шину, но я не знаю, когда процессор освобождает шину?

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

3 ответа

Решение
  1. LOCK это не сама инструкция: это префикс инструкции, который применяется к следующей инструкции. Эта инструкция должна быть чем-то, что делает чтение-изменение-запись в памяти (INC, XCHG, CMPXCHG и т.д.) --- в данном случае это incl (%ecx) инструкция, которая incвозражает lслово по адресу, указанному в ecx регистр.

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

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

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

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

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

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

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

От Google я знал, что инструкция блокировки приведет к тому, что процессор заблокирует шину, но я не знаю, когда процессор освободит автобус?

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

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

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

Минимальные исполняемые потоки 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.

Префикс LOCK используется для реализации:

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

Протестировано в Ubuntu 19.04 amd64.

Другие вопросы по тегам