C++ 什么是;锁;x86汇编中的指令意味着什么?
我在Qt的源代码中看到一些x86程序集:C++ 什么是;锁;x86汇编中的指令意味着什么?,c++,qt,assembly,x86,C++,Qt,Assembly,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 通过谷歌搜索,我知道l
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
lock
指令将导致CPU锁定总线,但我不知道CPU何时释放总线Add
LOCK
是一个指令前缀,因此它只适用于以下指令,这里的源代码不太清楚,但真正的指令是LOCK INC
。因此,总线在增量时锁定,然后解锁
关于上面的全部代码,我不明白这些代码是怎么写的
实现了Add
它们不实现加法,而是实现一个增量,如果旧值为0,则还提供一个返回指示。添加将使用LOCK XADD
(但是,windows InterlockedIncrement/Decreation也使用LOCK XADD
实现)
LOCK
不是指令本身:它是一个指令前缀,适用于以下指令。该指令必须是在内存上执行读-修改-写操作的指令(INC
,XCHG
,CMPXCHG
等)——在这种情况下,是incl(%ecx)
指令INC
在ecx
寄存器中保存的地址处重新输入l
ong字
LOCK
前缀确保CPU在操作期间对适当的缓存线拥有独占所有权,并提供某些额外的排序保证。这可以通过断言总线锁来实现,但CPU将尽可能避免这种情况。如果总线被锁定,则仅在锁定指令的持续时间内
ecx
寄存器中,然后执行lock incl(%ecx)
以原子方式递增该变量1。接下来的两条指令将eax
寄存器(保存函数返回值)设置为0(如果变量的新值为0),否则设置为1。该操作是一个增量操作,而不是一个添加操作(因此得名)
您可能无法理解的是,增加值所需的微码要求我们首先读入旧值 Lock关键字强制实际发生的多个微指令看起来是原子操作的 如果有两个线程分别尝试递增相同的变量,并且它们都同时读取相同的原始值,那么它们都递增到相同的值,并且都写出相同的值 与通常期望的变量递增两次不同,最终的结果是变量递增一次
锁定关键字防止发生这种情况。
< P> <强>最小可运行C++线程+锁内联汇编示例<强> < /P> 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;
}
可能的产出:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
由此我们可以看出,锁前缀使加法原子化:没有它,我们在许多加法上都有竞争条件,并且最后的总计数小于同步的20000
锁定前缀用于实现:
- C++11
:std::atomic
- C11
:atomic\u int
在Ubuntu19.04 amd64中测试。请参阅相关内容:我的回答解释了x86上的原子性,以及
锁前缀的确切作用,以及没有它会发生什么。谢谢!然后哪个寄存器存储函数(q_原子_增量)的返回值?返回值存储在%eaxSo中,代码:“return q_原子_增量(&_q_值)!=0”用于测试%eax是否不等于零?@gemfield:its zero'd,然后使用INC
中的条件标志通过SETNE
设置LSB。是在%eax中返回的旧值是0还是不是0(如答案当前所述),还是新值?因此指令“mov$0,%eax”似乎是多余的?@gemfield:No,mov
将所有eax
设置为零SETNE
仅更改低位字节。如果没有MOV
,EAX
的3个高字节将包含以前操作的随机剩余值,因此返回值将是不正确的。在俄罗斯的一本书“Assembler for DOS,WindowsêLinux,2000.Sergei Zukkov”中,作者提到了关于此前缀的以下内容:“在命令执行的所有时间内,如果有这样一个前缀,数据总线将被挂起,如果系统有不同的处理器,它将无法访问内存,直到带有前缀锁的命令结束。即使未指定锁前缀,XCHG命令也始终自动使用内存访问锁执行。此前缀只能与ADD、ADC和、BTC、BTR、BTS、CMPXCHG、DEC、INC、NEG、NOT或、SBB、SUB、XOR、XADD和XCHG命令一起使用。“@bruziuz:现代CPU的效率要高得多:如果lock
ed指令的数据没有穿过缓存线,CPU内核就可以在内部锁定该缓存线,而不是阻止所有其他内核的所有加载/存储。另请参阅我在上的回答,以了解有关如何使其对使用MESI缓存一致性协议的可能观察者显示原子的更多详细信息。非常感谢!酷!:)使用-O0
,并用一个完整的屏障(lock inc
)来限制非原子增量有什么意义?证明在最好的情况下它仍然是收支平衡的?如果您让non-lockedinc
从存储缓冲区向前移动,您会看到更多的计数丢失。@PeterCordes-O0
:没有花太多心思,默认情况下是为了更好地调试而做的,不过我最近注意到,这样做确实会让您更容易看到我这样简单的行为
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267