16

我是使用 gcc 内联汇编的新手,想知道在 x86 多核机器上,自旋锁(没有竞争条件)是否可以实现为(使用 AT&T 语法):

自旋锁:
mov 0 eax
锁定 cmpxchg 1 [lock_addr]
jnz spin_lock
ret

自旋解锁:
锁定 mov 0 [lock_addr]
ret
4

3 回答 3

26

你有正确的想法,但你的 asm 坏了:

cmpxchg不能使用立即操作数,只能使用寄存器。

lock不是 的有效前缀movmov到对齐的地址在 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。在locked 指令中做更少的工作可能会使高速缓存行固定的时间更短。有关使用and的完整 asm 实现,请参阅通过内联汇编实现内存操作的锁定(但仍然没有回退到操作系统辅助睡眠,只是无限期地旋转。)xchgpause

于 2011-08-04T02:36:52.230 回答
2

这将减少内存总线上的争用:

void spin_lock(int *p)
{
    while(!__sync_bool_compare_and_swap(p, 0, 1)) while(*p);
}
于 2012-10-16T21:22:22.547 回答
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存储在%rdiredister 中。

使用movlandtest而不是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 秒。

于 2019-12-23T12:41:02.227 回答