我正在尝试通过c使用cmpxchg和内联汇编。这是我的代码:
static inline int
cas(volatile void* addr, int expected, int newval) {
int ret;
asm volatile("movl %2 , %%eax\n\t"
"lock; cmpxchg %0, %3\n\t"
"pushfl\n\t"
"popl %1\n\t"
"and $0x0040, %1\n\t"
: "+m" (*(int*)addr), "=r" (ret)
: "r" (expected), "r" (newval)
: "%eax"
);
return ret;
}
这是我第一次使用内联,我不确定是什么原因导致了这个问题。 我也试过“cmpxchgl”,但仍然没有。还尝试取下锁。 我得到“操作数大小不匹配”。 我想也许这与我对addr的投射有关,但我不确定。我尝试用int交换int,所以不要真正理解为什么会出现大小不匹配的问题。 这是使用AT& T风格。 感谢
答案 0 :(得分:2)
您对cmpxchg指令的操作数顺序是相反的。 AT& T语法最后需要内存目标:
"lock; cmpxchg %3, %0\n\t"
或者您可以使用-masm=intel
以原始顺序编译该指令,但您的其余代码是AT& T语法和排序,因此这不是正确的答案。
至于它为什么说“操作数大小不匹配”,我只能说这似乎是一个汇编程序错误,因为它使用了错误的消息。
答案 1 :(得分:2)
正如@prl指出的那样,你颠倒了操作数,将它们置于英特尔顺序(See Intel's manual entry for cmpxchg
)。只要你的内联asm没有汇编,你应look at the asm the compiler was feeding to the assembler查看模板发生了什么。在您的情况下,只需删除static inline
,以便编译器创建一个独立的定义,然后得到(on the Godbolt compiler explorer):
movl %edx , %eax
lock; cmpxchg %ebx, (%ecx) # 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函数的通常语义是{ {1}}通过引用获取并在失败时更新。(至少C ++ 11 std :: atomic和C11 stdatomic如何使用bool atomic_compare_exchange_weak( volatile A *obj, C* expected, C desired );
进行更新。)
(弱/强的东西允许在使用LL/SC的目标上使用CAS重试循环的更好的代码,其中由于中断或用相同的值重写可能导致虚假失败.x86' s oldval
是"强")
实际上,GCC的遗留lock cmpxchg
内置提供了2个独立的CAS函数:一个返回旧值,另一个返回__sync
。两者都通过引用获取旧/新值。所以它与C ++ 11使用的API并不相同,但显然它并不是那么可怕,没有人使用它。
您过于复杂的代码无法移植到x86-64。从您使用bool
开始,我假设您是在x86-32上开发的。您不需要popl
来获得整数ZF;这是setcc
的用途。 cmpxchg example for 64 bit integer有一个32位的示例,以这种方式工作(以显示他们想要的64位版本)。
甚至更好,使用GCC6标志返回语法,因此在循环中使用它可以编译为pushf/pop
循环而不是cmpxchg / jne
/ cmpxchg
/ setz %al
/ { {1}}。
我们可以解决所有这些问题并改善寄存器分配。 (如果inline-asm语句的第一个或最后一个指令是test %al,%al
,则您可能无效地使用约束。)
当然,到目前为止,实际使用的最佳方法是使用C11 stdatomic或GCC内置。 https://gcc.gnu.org/wiki/DontUseInlineAsm如果编译器可以从代码中发出同样好(或更好)的asm,那么它就会理解",因为内联asm会约束编译器。它也很难正确/有效地编写和维护。
可移植到i386和x86-64,AT& T或Intel语法,适用于任何整数类型宽度的寄存器宽度或更小:
jnz
mov
是ASM方言的替代品。对于x86,它是// 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 }
是一个命名操作数约束;这是保持操作数的另一种方式。 The "=ccz"
takes the z
condition code as the output value,与{AT&T | Intel}
一样。
Compiles on Godbolt对于具有AT& T输出的32位x86的asm:
%[newval]
gcc是愚蠢的,并且在将setz
复制到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
之前将0
存储在一个注册表中,而不是在循环内重新归零eax
。这就是它需要保存/恢复EBX的原因。尽管如此(来自x86 spinlock using cmpxchg),它与我们避免使用inline-asm的结果相同:
eax
有人应该教gcc,英特尔CPU可以通过xor-zeroing更便宜地实现// 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));
}
,而不是使用mov复制它,特别是在Sandybridge(0
- 零消除但没有xor
}在剔除)。