Что означает инструкция «lock» в сборке x86? - PullRequest
52 голосов
/ 17 января 2012

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

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

Ответы [ 4 ]

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

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

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

12 голосов
/ 24 марта 2013

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

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

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

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

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

10 голосов
/ 17 января 2012

Из гугла я знал, что инструкция блокировки вызовет блокировку процессора процессором, но Не знаете, когда процессор освободит автобус?

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

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

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

0 голосов

Минимальный работающий поток 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 upstream .

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

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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...