我是使用 gcc 内联汇编的新手,想知道在 x86 多核机器上,自旋锁(没有竞争条件)是否可以实现为(使用 AT&T 语法):
自旋锁: mov 0 eax 锁定 cmpxchg 1 [lock_addr] jnz spin_lock ret 自旋解锁: 锁定 mov 0 [lock_addr] ret
我是使用 gcc 内联汇编的新手,想知道在 x86 多核机器上,自旋锁(没有竞争条件)是否可以实现为(使用 AT&T 语法):
自旋锁: mov 0 eax 锁定 cmpxchg 1 [lock_addr] jnz spin_lock ret 自旋解锁: 锁定 mov 0 [lock_addr] ret
你有正确的想法,但你的 asm 坏了:
cmpxchg
不能使用立即操作数,只能使用寄存器。
lock
不是 的有效前缀mov
。 mov
到对齐的地址在 x86 上是原子的,所以无论如何你都不需要lock
。
自从我使用 AT&T 语法以来已经有一段时间了,希望我记得一切:
spin_lock:
xorl %ecx, %ecx
incl %ecx # newVal = 1
spin_lock_retry:
xorl %eax, %eax # expected = 0
lock; cmpxchgl %ecx, (lock_addr)
jnz spin_lock_retry
ret
spin_unlock:
movl $0, (lock_addr) # atomic release-store
ret
请注意,GCC 具有原子内置函数,因此您实际上不需要使用内联 asm 来完成此操作:
void spin_lock(int *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1));
}
void spin_unlock(int volatile *p)
{
asm volatile ("":::"memory"); // acts as a memory barrier.
*p = 0;
}
正如 Bo 在下面所说,锁定指令会产生成本:您使用的每个指令都必须获得对高速缓存行的独占访问权并在运行时将其锁定lock cmpxchg
,就像对该高速缓存行的正常存储但在lock cmpxchg
执行期间保持不变。这可能会延迟解锁线程,尤其是在多个线程正在等待获取锁的情况下。即使没有很多 CPU,它仍然很容易并且值得优化:
void spin_lock(int volatile *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1))
{
// spin read-only until a cmpxchg might succeed
while(*p) _mm_pause(); // or maybe do{}while(*p) to pause first
}
}
pause
当您的代码像这样旋转时,该指令对于超线程 CPU 的性能至关重要——它允许第二个线程在第一个线程旋转时执行。在不支持的 CPU 上pause
,它被视为nop
.
pause
还可以防止离开自旋循环时的内存顺序错误推测,当终于到了再次做实际工作的时候。 x86 中“PAUSE”指令的目的是什么?
请注意,自旋锁实际上很少使用:通常,使用临界区或 futex 之类的东西。它们集成了自旋锁以在低竞争下提高性能,但随后又退回到操作系统辅助的睡眠和通知机制。他们还可能采取措施来提高公平性,以及cmpxchg
/pause
循环不做的许多其他事情。
另请注意,cmpxchg
对于简单的自旋锁来说,这不是必需的:您可以使用xchg
然后检查旧值是否为 0。在lock
ed 指令中做更少的工作可能会使高速缓存行固定的时间更短。有关使用and的完整 asm 实现,请参阅通过内联汇编实现内存操作的锁定(但仍然没有回退到操作系统辅助睡眠,只是无限期地旋转。)xchg
pause
这将减少内存总线上的争用:
void spin_lock(int *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1)) while(*p);
}
语法错误。稍作修改后即可使用。
spin_lock:
movl $0, %eax
movl $1, %ecx
lock cmpxchg %ecx, (lock_addr)
jnz spin_lock
ret
spin_unlock:
movl $0, (lock_addr)
ret
提供运行速度更快的代码。假设lock_addr
存储在%rdi
redister 中。
使用movl
andtest
而不是lock cmpxchgl %ecx, (%rdi)
旋转。
仅在lock cmpxchgl %ecx, (%rdi)
有机会时才尝试进入临界区。
然后可以避免不必要的总线锁定。
spin_lock:
movl $1, %ecx
loop:
movl (%rdi), %eax
test %eax, %eax
jnz loop
lock cmpxchgl %ecx, (%rdi)
jnz loop
ret
spin_unlock:
movl $0, (%rdi)
ret
我已经使用 pthread 和这样的简单循环对其进行了测试。
for(i = 0; i < 10000000; ++i){
spin_lock(&mutex);
++count;
spin_unlock(&mutex);
}
在我的测试中,第一个需要 2.5~3 秒,第二个需要 1.3~1.8 秒。