Gcc 使用cmpxchg的x86自旋锁

Gcc 使用cmpxchg的x86自旋锁,gcc,assembly,synchronization,x86,spinlock,Gcc,Assembly,Synchronization,X86,Spinlock,我不熟悉使用gcc内联汇编,我想知道在x86多核机器上,自旋锁(没有竞争条件)是否可以实现为(使用AT&T语法): 旋转锁: mov0eax 锁定cmpxchg 1[锁定地址] jnz自旋锁 ret 旋转解锁: 锁定mov 0[锁定地址] ret 你的想法是对的,但是你的asm坏了: cmpxchg无法使用立即数操作数,只能使用寄存器 lock不是mov的有效前缀mov到对齐地址是原子的,所以您不需要lock 我已经有一段时间没有使用AT&T语法了,希望我记住了一切: spin_lock:

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

旋转锁: mov0eax 锁定cmpxchg 1[锁定地址] jnz自旋锁 ret 旋转解锁: 锁定mov 0[锁定地址] ret
你的想法是对的,但是你的asm坏了:

cmpxchg
无法使用立即数操作数,只能使用寄存器

lock
不是
mov
的有效前缀<在x86上,code>mov到对齐地址是原子的,所以您不需要
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
执行期间保持。这会延迟解锁线程,特别是当多个线程正在等待获取锁时。即使没有很多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上,它被视为
nop

pause
还可以防止在离开旋转循环时内存顺序的错误推测,此时该是再次执行实际工作的时候了

请注意,旋转锁实际上很少使用:通常使用临界截面或futex之类的东西。它们集成了一个自旋锁以在低争用情况下实现性能,但随后又回到了操作系统辅助的睡眠和通知机制。他们还可能采取措施来提高公平性,以及许多其他
cmpxchg
/
pause
循环不能做的事情



还要注意,对于简单的自旋锁,
cmpxchg
是不必要的:您可以使用
xchg
,然后检查旧值是否为0。在
lock
ed指令中执行较少的工作可能会使缓存线固定的时间更短。有关使用
xchg
pause
的完整asm实现,请参阅(但仍然没有回退到操作系统辅助睡眠,只是无限期地旋转)。

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

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\u addr
存储在
%rdi
redister中

使用
movl
test
代替
lock cmpxchgl%ecx、(%rdi)
旋转

使用
锁定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);
}
for(i=0;i<10000000;++i){
自旋锁(和互斥锁);
++计数;
自旋解锁(和互斥);
}

在我的测试中,第一个需要2.5~3秒,第二个需要1.3~1.8秒。

是否应该将void spin_lock()的参数也声明为volatile?否。
\u sync\u bool\u compare\u和\u swap
已经将其视为
volatile
。在
spin\u unlock
中用作内存屏障的asm应该包括内存缓冲。尽管另一方面,还有
\uuuu sync\u lock\u release
,它的设计只是为了做“写屏障,写0”的事情,根本不需要考虑asm,甚至“有点便携”。它没有显式地作为读屏障工作(在目标体系结构上也有),但这没关系。最糟糕的情况是另一个线程在一个罕见的、不太可能的情况下执行一个额外的自旋。我认为实际的自旋锁应该在尽可能短的序列中实现。由于我们可以在vlock值为0时锁定(我们将其替换为1并返回0),因此更自然的顺序是调用lock spinlock_failed,当我们返回1时,这将是真的,即锁定失败。然后,可以围绕spinlock_在重试时失败等构建附加功能。您应该在纯负载上旋转(在循环中使用
pause
),而不是像在C中那样
lock cmpxchg
。在首次尝试加载之前,在cmpxchg失败之后,您的C版本可能应该是
do{}while()
循环到
pause
。我不确定这是否会导致内存顺序错误推测(这可以避免
pause
的错误推测),但如果是这样,它会刷新整个管道,影响两个超线程,而不仅仅是旋转的超线程。同意,尽管这段代码不是很好。编译器可以很容易地优化出一个简单的while(*p)。增加一些障碍。另外,为英特尔芯片添加_mm_pause()可以显著提高性能。@Bo。这需要
易失性
\u原子性
。循环将优化为
if(*p){while(1);}
,即如果进入无限循环。如果要调整它以提高效率,也可以使用
pause
,以避免在离开只读循环的迭代上出现内存顺序错误推测管道核。(但您确实希望避免在无争用快速路径上运行
pause
,因此您必须重新安排分支。)例如,如下答案:
for(i = 0; i < 10000000; ++i){
    spin_lock(&mutex);
    ++count;
    spin_unlock(&mutex);
}