使用cmpxchg的x86螺旋锁

时间:2011-08-04 02:15:18

标签: gcc assembly synchronization x86 spinlock

我是使用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

3 个答案:

答案 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指令中执行较少的工作可能会使缓存行固定更短的时间。有关使用xchgpause的完整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重存储中。

使用movltest而非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秒。