正如@prl 指出的那样,您颠倒了操作数,将它们按 Intel 顺序排列 (See Intel's manual entry for cmpxchg)。任何时候你的内联汇编没有组装,你应该look at the asm the compiler was feeding to the assembler 看看你的模板发生了什么。在您的情况下,只需删除 static inline 以便编译器进行独立定义,然后您会得到 (on the Godbolt compiler explorer):
# gcc -S output for the original, with cmpxchg operands backwards
movl %edx , %eax
lock; cmpxchg (%ecx), %ebx # error on this line from the assembler
pushfl
popl %edx
and $0x0040, %edx
有时这会提示你的眼睛/大脑在没有盯着%3 和%0 的情况下,特别是在你检查instruction-set reference manual entry for cmpxchg 并看到内存操作数是目标之后(英特尔语法第一个操作数, AT&T 语法最后一个操作数)。
这是有道理的,因为显式寄存器操作数只是一个源,而 EAX 和内存操作数都被读取,然后根据比较的成功写入其中一个或另一个。 (从语义上讲,您使用cmpxchg 作为内存目标的条件存储。)
您正在丢弃 cas-failure 案例中的加载结果。我想不出 cmpxchg 的任何用例,其中单独加载原子值是不正确的,而不仅仅是效率低下,但 CAS 函数的通常语义是采用 oldval通过引用并在失败时更新。(至少 C++11 std::atomic 和 C11 stdatomic 使用bool atomic_compare_exchange_weak( volatile A *obj, C* expected, C desired ); 是这样做的。)
(弱/强的东西允许在使用LL/SC的目标上为CAS重试循环提供更好的代码生成,其中可能由于中断或用相同的值重写而导致虚假失败。x86的lock cmpxchg是“强”)
实际上,GCC 遗留的 __sync 内置函数提供了 2 个独立的 CAS 函数:一个返回旧值,一个返回 bool。两者都通过引用获取旧/新值。所以它与 C++11 使用的 API 不同,但显然它并没有那么可怕,以至于没有人使用它。
您过于复杂的代码无法移植到 x86-64。从您对popl 的使用来看,我假设您是在 x86-32 上开发的。你不需要pushf/pop 来获取 ZF 作为整数;这就是setcc 的用途。 cmpxchg example for 64 bit integer 有一个 32 位的例子就是这样工作的(展示他们想要 64 位版本的)。
或者更好的是,使用 GCC6 标志返回语法,这样在循环中使用它可以编译为 cmpxchg / jne 循环而不是 cmpxchg / setz %al / test %al,%al / jnz。
我们可以解决所有这些问题并改进寄存器分配。 (如果 inline-asm 语句的第一条或最后一条指令是 mov,则您可能使用约束效率低下。)
当然,到目前为止,实际使用的最佳方式是使用 C11 stdatomic 或内置 GCC。 https://gcc.gnu.org/wiki/DontUseInlineAsm 在编译器可以从它“理解”的代码中发出同样好(或更好)的 asm 的情况下,因为内联 asm 限制了编译器。它也很难正确/高效地编写和维护。
可移植到 i386 和 x86-64、AT&T 或 Intel 语法,并且适用于寄存器宽度或更小的任何整数类型宽度:
// Note: oldVal by reference
static inline char CAS_flagout(int *ptr, int *poldVal, int newVal)
{
char ret;
__asm__ __volatile__ (
" lock; cmpxchg {%[newval], %[mem] | %[mem], %[newval]}\n"
: "=@ccz" (ret), [mem] "+m" (*ptr), "+a" (*poldVal)
: [newval]"r" (newVal)
: "memory"); // barrier for compiler reordering around this
return ret; // ZF result, 1 on success else 0
}
// spinning read-only is much better (with _mm_pause in the retry loop)
// not hammering on the cache line with lock cmpxchg.
// This is over-simplified so the asm is super-simple.
void cas_retry(int *lock) {
int oldval = 0;
while(!CAS_flagout(lock, &oldval, 1)) oldval = 0;
}
{ foo,bar | bar,foo } 是 ASM 方言的替代品。对于 x86,它是 {AT&T | Intel}。 %[newval] 是命名操作数约束;这是另一种保留操作数的方法。 The "=ccz" takes the z condition code as the output value,比如setz。
Compiles on Godbolt 到此 asm,用于带有 AT&T 输出的 32 位 x86:
cas_retry:
pushl %ebx
movl 8(%esp), %edx # load the pointer arg.
movl $1, %ecx
xorl %ebx, %ebx
.L2:
movl %ebx, %eax # xor %eax,%eax would save a lot of insns
lock; cmpxchg %ecx, (%edx)
jne .L2
popl %ebx
ret
gcc 是愚蠢的,在将0 复制到eax 之前将其存储在一个reg 中,而不是在循环内重新归零eax。这就是为什么它需要保存/恢复 EBX。不过,这与我们通过避免 inline-asm 得到的 asm 相同(来自 x86 spinlock using cmpxchg):
// also omits _mm_pause and read-only retry, see the linked question
void spin_lock_oversimplified(int *p) {
while(!__sync_bool_compare_and_swap(p, 0, 1));
}
有人应该告诉 gcc,英特尔 CPU 可以使用 xor-zeroing 实现 0 比使用 mov 复制它更便宜,尤其是在 Sandybridge 上(xor-zeroing 消除,但没有 mov-elimination)。