我是使用gcc内联汇编的新手,并且想知道在x86多核机器上是否可以实现自旋锁(没有竞争条件)(使用AT& T语法):
spin_lock: mov 0 eax lock cmpxchg 1 [lock_addr] jnz spin_lock ret spin_unlock: lock mov 0 [lock_addr] ret
答案 0 :(得分:24)
你有正确的想法,但你的主题已经崩溃了:
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所说,锁定的指令需要付出代价:您使用的每一个都必须acquire exclusive access to the cache line and lock it down while lock cmpxchg
runs,就像对该缓存行的正常存储一样,但在执行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
指令对HyperThreading CPU的性能至关重要 - 它让第二个线程在第一个线程旋转时执行。在不支持pause
的CPU上,它被视为nop
。
pause
还可以在离开自旋循环时防止内存顺序错误推测,当它终于再次进行实际工作时。 What is the purpose of the "PAUSE" instruction in x86?
请注意,旋转锁实际上很少使用:通常,使用类似临界区或futex的东西。这些集成了自旋锁以在低争用下实现性能,但随后又回退到OS辅助的睡眠和通知机制。他们也可能采取措施来提高公平性,以及cmpxchg
/ pause
循环所做的许多其他事情。
另请注意,cmpxchg
对于简单的自旋锁是不必要的:您可以使用xchg
然后检查旧值是否为0。在lock
ed指令中执行较少的工作可能会使缓存行固定更短的时间。有关使用xchg
和pause
的完整asm实现,请参阅Locks around memory manipulation via inline assembly(但仍然没有回退到操作系统辅助睡眠,只是无限期地旋转。)
答案 1 :(得分:2)
这将减少对内存总线的争用:
void spin_lock(int *p)
{
while(!__sync_bool_compare_and_swap(p, 0, 1)) while(*p);
}
答案 2 :(得分:0)
语法错误。稍加修改即可使用。
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
重存储中。
使用movl
和test
而非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秒。