x86汇编中的“锁定”指令是什么意思?


69

我在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



1
相关:我对num ++的回答是否可以原子表示“ int num”?解释了x86上的原子性,以及lock前缀的确切含义以及不带前缀的情况。
彼得·科德斯

Answers:


103
  1. LOCK本身不是指令:它是指令前缀,适用于以下指令。该指令必须的东西做内存(读-修改-写INCXCHGCMPXCHG等)---在这种情况下它是incl (%ecx)哪个指令increments的l在举行的地址翁字ecx寄存器。

    LOCK前缀可确保CPU具有操作的持续时间适当的高速缓存线的独占所有权,并提供某些额外订购的保证。这可以通过声明总线锁定来实现,但是CPU会尽可能避免这种情况。如果总线被锁定,则仅在锁定指令期间。

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


因此,指示“ mov $ 0,%eax”似乎多余吗?
gemfield 2012年

4
@gemfield:不,MOV将全部设置EAX为零。SETNE仅更改低字节。如果没有MOV,则的3个高字节EAX将包含先前操作的随机剩余值,因此返回值将不正确。
安东尼·威廉姆斯

1
在俄罗斯的一本书《用于DOS的汇编程序,Windows®Linux,2000年。Sergei Zukkov》中,作者提到了以下有关该前缀的内容:“在命令的所有时间内,都提供了这样的前缀,数据总线将被挂起,如果系统具有不同的处理器,则直到带有前缀LOCK的命令结束后才能访问内存,即使未指定LOCK前缀,XCHG命令也会始终自动执行带有内存访问锁定的操作。仅使用命令ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD和XCHG。
bruziuz

3
@bruziuz:现代的CPU是多少更有效:如果一个数据lockED指令不跨越高速缓存线,CPU核心可以只在内部锁定的,而不是阻止所有加载/存储与所有其他内核,高速缓存行。另请参阅我对num ++是否可以原子表示“ int num”的回答有关如何使用MESI缓存一致性协议使它对可能的观察者显得原子化的更多详细信息。
彼得·科德斯

非常感谢你!凉!:)
bruziuz

13

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

Lock关键字强制实际发生的多个微指令似乎是原子操作的。

如果您有2个线程分别尝试递增相同的变量,并且它们都同时读取相同的原始值,那么它们都将递增至相同的值,并且都写出相同的值。

您不必将变量递增两次(这是通常的期望),而是最终将变量递增一次。

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


11

从谷歌,我知道锁定指令将导致CPU锁定总线,但我不知道何时CPU释放总线?

LOCK是指令的前缀,因此仅适用于以下指令,此处的源代码并不清楚,但实际的指令是LOCK INC。因此,将总线锁定为增量,然后将其解锁

关于以上整个代码,我不了解这些代码如何实现Add?

他们没有实现加法,而是实现了增量,以及如果旧值是0,则实现了返回指示。将使用加法LOCK XADD(但是,也可以使用实现Windows InterlockedIncrement / Decrement LOCK XADD)。


谢谢!然后哪个寄存器存储函数(q_atomic_increment)的返回值?
gemfield 2012年

1
返回值存储在%eax中
2012年

因此,代码:“返回q_atomic_increment(&_ q_value)!= 0”是测试%eax是否不等于零?
gemfield 2012年

@gemfield:将其置零,然后SETNE使用中的条件标志设置LSB INC
Necrolis 2012年

是%eax(如当前答案所示)中返回的值是否为0,还是值?
2015年

2

最少的可运行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中测试。


使用-O0,并用完整的势垒(lock inc)围住非原子增量有什么意义?为了证明即使在最佳情况下它仍然会损坏?如果您inc从存储缓冲区中进行非锁定转发,则会看到更多的丢失计数。
彼得·科德斯

@PeterCordes -O0:并没有考虑太多,默认情况下是为了更好的调试而做的,尽管我laster注意到在这样一个简单的情况下这样做确实使查看行为变得容易一些,因为-O3optimize循环到单个添加。“并用完整的屏障围住非原子增量”:LOCK是否还会影响上述程序中的非原子变量?
西罗Santilli郝海东冠状病六四事件法轮功

1
lock inc是一个完整的障碍,例如mfence。您没有4个单独的循环,而是交错增量。它不会使另一个inc 原子化,但是会强制inc在下一个inc加载之前将其存储全局可见,因此是的,它会对其产生重大影响。如果你不想-O3跳绳做+= N,你可以使用volatile; 限制代码生成而不给出任何原子性volatile是为了什么。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.