我正在考虑通过gcc查看为原子操作生成的一些程序集。我尝试了以下短序列:
int x1;
int x2;
int foo;
void test()
{
__atomic_store_n( &x1, 1, __ATOMIC_SEQ_CST );
if( __atomic_load_n( &x2 ,__ATOMIC_SEQ_CST ))
return;
foo = 4;
}
看看Herb Sutter的原子武器谈论代码生成,他提到X86手册要求xchg
用于原子存储,而简单mov
用于原子读取。所以我期待的是:
test():
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, %eax
xchg %eax, x1(%rip)
movl x2(%rip), %eax
testl %eax, %eax
setne %al
testb %al, %al
je .L2
jmp .L1
.L2:
movl $4, foo(%rip)
.L1:
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
由于锁定的xchg
指令,内存栅栏是隐式的。
但是,如果我使用gcc -march=core2 -S test.cc
进行编译,我会得到以下结果:
test():
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, %eax
movl %eax, x1(%rip)
mfence
movl x2(%rip), %eax
testl %eax, %eax
setne %al
testb %al, %al
je .L2
jmp .L1
.L2:
movl $4, foo(%rip)
.L1:
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
因此,gcc不使用xchg
操作,而是使用mov + mfence
组合。这个代码生成的原因是什么,它与x86架构根据Herb Sutter强制要求的那个不同?
答案 0 :(得分:11)
当目标是内存位置时,xchg
指令隐含了锁定语义。这意味着您可以原子方式将寄存器的内容与内存位置的内容进行交换。
问题中的例子是做一个原子商店,而不是交换。 x86架构内存模型保证在多处理器/多核系统中,由一个线程完成的存储将由其他线程以该顺序看到......因此内存移动就足够了。话虽如此,有较旧的Intel CPU和一些克隆,这个区域存在错误,并且需要xchg
作为这些CPU的解决方法。请参阅此维基百科关于自旋锁的文章的重要优化部分:
http://en.wikipedia.org/wiki/Spinlock#Example_implementation
哪个州
上面的简单实现适用于使用x86架构的所有CPU。但是,可以进行一些性能优化:
在x86架构的后续实现中,spin_unlock可以安全地使用解锁的MOV而不是速度较慢的锁定XCHG。这是由于微妙的内存排序规则支持这一点,即使MOV不是一个完整的内存屏障。但是,某些处理器(一些Cyrix处理器,一些Intel Pentium Pro修订版(由于错误)以及早期的Pentium和i486 SMP系统)将做错事,受锁保护的数据可能会被破坏。在大多数非x86体系结构中,必须使用显式内存屏障或原子指令(如示例中所示)。在某些系统上,例如IA-64,有一些特殊的“解锁”指令可以提供所需的内存排序。
内存屏障mfence
确保所有存储都已完成(CPU核心中的存储缓冲区为空并且值存储在缓存或内存中),它还确保将来的加载不会无序执行。
事实上MOV足以解锁互斥锁(不需要序列化或内存障碍)这一事实在1999年英特尔架构师对Linus Torvalds的回复中得到了“正式”澄清
http://lkml.org/lkml/1999/11/24/90
我想后来发现它对某些旧的x86处理器不起作用。