Gcc 使用cmpxchg的x86自旋锁
我不熟悉使用gcc内联汇编,我想知道在x86多核机器上,自旋锁(没有竞争条件)是否可以实现为(使用AT&T语法): 旋转锁: mov0eax 锁定cmpxchg 1[锁定地址] jnz自旋锁 ret 旋转解锁: 锁定mov 0[锁定地址] retGcc 使用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:
你的想法是对的,但是你的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);
}