ChatGPT解决这个技术问题 Extra ChatGPT

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

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

从谷歌搜索,我知道锁定指令会导致 CPU 锁定总线,但我不知道 CPU 何时释放总线?关于上面的整个代码,我不明白这段代码是如何实现 Add?

相关:我在 Can num++ be atomic for 'int num'? 上的回答解释了 x86 上的原子性,以及 lock 前缀究竟做了什么,没有它会发生什么。

C
Community

LOCK 本身不是指令:它是指令前缀,适用于后面的指令。该指令必须是对内存(INC、XCHG、CMPXCHG 等)执行读-修改-写操作的指令——在这种情况下,它是 incl (%ecx) 指令,它在ecx 寄存器。 LOCK 前缀确保 CPU 在操作期间拥有相应缓存行的独占所有权,并提供某些额外的排序保证。这可以通过断言总线锁定来实现,但 CPU 将尽可能避免这种情况。如果总线被锁定,则仅在锁定指令的持续时间内。此代码将要从堆栈中递增的变量的地址复制到 ecx 寄存器中,然后它会锁定 incl (%ecx) 以原子地将该变量递增 1。接下来的两条指令设置 eax 寄存器(它保存返回值如果变量的新值为 0,则为 0,否则为 1。该操作是增量,而不是添加(因此得名)。


所以指令“mov $0,%eax”似乎是多余的?
@gemfield:不,MOV 将所有 EAX 设置为零。 SETNE 只改变低字节。如果没有 MOV,则 EAX 的 3 个高字节将包含先前操作的随机剩余值,因此返回值将不正确。
在俄罗斯的一本书“Assembler for DOS, Windows и Linux, 2000. Sergei Zukkov”中,作者提到了关于这个前缀的以下内容:如果一个系统有不同的处理器,它不能访问内存,直到带有前缀 LOCK 的命令结束。XCHG 命令总是自动执行内存访问锁,即使没有指定 LOCK 前缀。可以使用这个前缀仅适用于命令 ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD 和 XCHG。
@bruziuz:现代 CPU 更加更高效:如果 locked 指令的数据没有跨越缓存线,CPU 内核可以在内部锁定该缓存线,而不是阻塞所有负载/stores 来自所有其他核心。另请参阅我在 Can num++ be atomic for 'int num'? 上的回答,了解如何使用 MESI 缓存一致性协议使其对可能的观察者显示原子。
非常感谢!凉爽的! :)
D
Dan

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

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

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

不是让变量增加两次,这是典型的期望,而是最终增加一次变量。

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


N
Necrolis

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

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

关于上面的整个代码,我不明白这些代码是如何实现 Add?

它们不实现添加,它们实现增量,以及如果旧值为 0 的返回指示。添加将使用 LOCK XADD(但是,Windows InterlockedIncrement/Decrement 也使用 LOCK XADD 实现)。


谢谢!那么哪个寄存器存储函数(q_atomic_increment)的返回值?
返回值存储在 %eax
所以,代码:“return q_atomic_increment(&_q_value) != 0”是为了测试 %eax 是否不等于零?
@gemfield:将其归零,然后使用 INC 中的条件标志通过 SETNE 设置 LSB。
在 %eax 中返回的旧值是否为 0 (如当前答案所述),还是新值?
C
Ciro Santilli Путлер Капут 六四事

最小可运行 C++ 线程 + LOCK 内联汇编示例

主文件

#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。

LOCK前缀用于实现:

C++11 std::atomic:std::atomic 到底是什么?

C11 atomic_int:如何在纯 C 中启动线程?

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

在 Ubuntu 19.04 amd64 中测试。


使用 -O0 并用完整的屏障 (lock inc) 隔离非原子增量有什么意义?为了证明即使在最好的情况下它仍然被破坏?如果您让未锁定的 inc 从存储缓冲区转发,您会看到更多的丢失计数。
@PeterCordes -O0:并没有考虑太多,默认情况下这样做是为了更好地调试,尽管我最后注意到它确实让我更容易看到这种简单情况下的行为,因为 -O3 将循环优化为一个添加。 “并用一个完整的障碍围住非原子增量”:LOCK 是否也会影响上述程序中的非原子变量?
lock inc 是一个完整的障碍,如 mfence。您没有 4 个单独的循环,而是交错增量。它不会使其他 inc 原子,但它会强制 inc 的存储在下一个 inc 加载之前全局可见,所以是的,它会显着影响它。如果您不想让 -O3 跳出循环并执行 += N,则可以使用 volatilevolatile 的作用是在不提供任何原子性的情况下约束代码生成。