“锁定”指令在x86汇编中意味着什么?

时间:2012-01-17 07:33:34

标签: c++ qt assembly

我在Qt的源代码中看到了一些x86程序集:

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. 从谷歌搜索,我知道lock指令将导致CPU锁定总线,但我不知道CPU何时释放总线?

  2. 关于以上整个代码,我不明白此代码如何实现Add

4 个答案:

答案 0 :(得分:77)

  1. LOCK本身不是指令:它是一个指令前缀,适用于以下指令。该指令必须是对内存执行读取 - 修改 - 写入的内容(INCXCHGCMPXCHG等等 - 在这种情况下,它是incl (%ecx) incl寄存器中的地址处对ecx ong字进行修改的说明。

    LOCK前缀可确保CPU在操作期间拥有相应缓存行的独占所有权,并提供某些额外的排序保证。这可以通过断言总线锁来实现,但CPU会尽可能避免这种情况。如果总线被锁定,则仅在锁定指令的持续时间内。

  2. 此代码将要从堆栈递增的变量的地址复制到ecx寄存器中,然后它lock incl (%ecx)将该变量原子递增1.接下来的两个指令集如果变量的新值为0,则eax寄存器(保存函数的返回值)为0,否则为1。操作是增量,而不是添加(因此名称)。

答案 1 :(得分:12)

您可能无法理解的是,增加值所需的微码需要我们首先读取旧值。

Lock关键字强制实际发生的多个微指令似乎以原子方式运行。

如果你有2个线程试图递增相同的变量,并且它们同时读取相同的原始值,那么它们都会增加到相同的值,并且它们都会写出相同的值。

不是让变量递增两次,这是典型的期望,你最终会将变量递增一次。

lock关键字可以防止这种情况发生。

答案 2 :(得分:10)

  

从google,我知道锁定指令会导致cpu锁定总线,但是我   不知道什么时候cpu释放公交车?

LOCK是一个指令前缀,因此它只适用于下面的指令,来源并没有在这里说得很清楚,但实际指令是LOCK INC。因此总线锁定增量,然后解锁

  

关于以上整个代码,我不明白这些代码是怎样的   实施了Add?

他们没有实现Add,他们实现了一个增量,如果旧值是0,则返回指示。添加将使用LOCK XADD(但是,Windows InterlockedIncrement / Decrement也会实现LOCK XADD)。

答案 3 :(得分: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。

另请参阅:What does multicore assembly language look like?

在Ubuntu 19.04 amd64中进行了测试。