我正在用c ++编写多线程应用程序,其中性能至关重要。我需要在线程之间复制小结构时使用大量锁定,为此我选择使用自旋锁。
我已经对此进行了一些研究和速度测试,我发现大多数实现大致同样快:
__asm {}
使用CMPXCHG8B
之类的内联汇编,并使用大约70个时间单位,,但我不确定是否已创建了正确的内存屏障。 编辑:这里给出的时间是2个线程锁定和解锁螺旋锁1,000,000次所需的时间。
我知道这并没有很大区别但是因为自旋锁是一个使用频繁的对象,人们会认为程序员会同意以最快的方式制作自旋锁。谷歌搜索导致许多不同的方法。我认为如果使用内联汇编并使用指令__asm
{
spin_lock:
;locking code.
spin_unlock:
;unlocking code.
}
而不是比较32位寄存器来实现this code将是最快的。 此外,必须考虑内存障碍,这可以通过LOCK CMPXHG8B(我认为?)来完成,这保证了核心之间共享内存的“专有权”。最后[有人建议]对于繁忙的等待应该伴随NOP:REP ,这将使超线程处理器能够切换到另一个线程,但我不确定这是否属实? / p>
从我对不同螺旋锁的性能测试中可以看出,没有太大的区别,但是出于纯粹的学术目的,我想知道哪一个是最快的。然而,由于我在汇编语言和内存障碍方面的经验非常有限,如果有人可以编写我在LOCK CMPXCHG8B和适当的内存屏障中提供的最后一个示例的汇编代码,我会很高兴。以下模板:
{{1}}
答案 0 :(得分:10)
虽然已经有一个已经接受的答案,但有一些可以用来改善所有答案的内容,取自this Intel article, all above fast lock implementation:
答案 1 :(得分:6)
我通常不会抱怨有人努力实现快速代码:它通常是一个非常好的练习,可以更好地理解编程和更快的代码。
我也不会抱怨这里,但我可以毫不含糊地说明快速自旋锁3指令的问题很长或者更多 - 至少在x86架构上 - 是徒劳的追逐。
这就是原因:
使用典型代码序列调用自旋锁
lock_variable DW 0 ; 0 <=> free
mov ebx,offset lock_variable
mov eax,1
xchg eax,[ebx]
; if eax contains 0 (no one owned it) you own the lock,
; if eax contains 1 (someone already does) you don't
释放自旋锁是微不足道的
mov ebx,offset lock_variable
mov dword ptr [ebx],0
xchg指令引发处理器上的锁定引脚,这实际上意味着我希望在接下来的几个时钟周期内使用总线。该信号通过高速缓存进行编织,然后进入最慢的总线主控设备,该设备通常是PCI总线。当每个总线设备完成时,锁定(锁定确认)信号被发回。然后进行实际的交换。问题是锁定/锁定序列需要很长时间。 PCI总线可以以33MHz运行,具有几个延迟周期。在3.3 GHz CPU上,这意味着每个PCI总线周期需要100个CPU周期。
根据经验,我认为锁定需要300到3000个CPU周期来完成,最后我不知道我是否拥有锁。因此,您可以通过&#34;快速&#34; spinlock将是海市蜃楼,因为没有锁定就像下一个,它将取决于您在短时间内的公交车情况。
________________ EDIT ________________
我刚刚读到螺旋锁是一个使用频繁的物体。&#34;好吧,你显然不明白自旋锁占用了大量的CPU周期。或者,换句话说,每次调用它都会失去大量的处理能力。
使用自旋锁(或更大的兄弟,关键部分)时的技巧是尽可能少地使用它们,同时仍然实现预期的程序功能。在整个地方使用它们很容易,结果你的表现会很黯淡。
这不仅仅是编写快速代码,也是关于组织数据的。当你写&#34;在线程之间复制小结构时#34;你应该意识到锁的完成时间比实际复制要长几百倍。
________________ EDIT ________________
当您计算平均锁定时间时,它可能会说很少,因为它在您的机器上测量可能不是预期的目标(可能具有完全不同的总线使用特性)。对于您的机器,平均值将由个别非常快的时间(当总线主控活动没有干扰时)一直到非常慢的时间(当总线主控干扰很大时)。
您可以引入用于确定最快和最慢情况的代码,并计算商数以查看螺旋锁时间可以变化的程度。
________________ EDIT ________________
2016年5月更新。
彼得·科德斯(Peter Cordes)提出这样的想法:“在无争议案件中调整锁定是有意义的”#34;并且在现代CPU上不会发生数百个时钟周期的锁定时间,除非锁定变量未对齐。我开始想知道我的previous test program - 用32位Watcom C编写 - 可能会受到WOW64的阻碍,因为它运行在64位操作系统上:Windows 7。
所以我写了一个64位程序并用TDM&#39; gcc 5.3编译它。该程序利用隐式总线锁定指令变体&#34; XCHG r,m&#34;用于锁定和简单分配&#34; MOV m,r&#34;用于解锁。在某些锁定变体中,锁定变量经过预先测试,以确定是否可以尝试锁定(使用简单的比较&#34; CMP r,m&#34;可能不会在L3外冒险)。这是:
// compiler flags used:
// -O1 -m64 -mthreads -mtune=k8 -march=k8 -fwhole-program -freorder-blocks -fschedule-insns -falign-functions=32 -g3 -Wall -c -fmessage-length=0
#define CLASSIC_BUS_LOCK
#define WHILE_PRETEST
//#define SINGLE_THREAD
typedef unsigned char u1;
typedef unsigned short u2;
typedef unsigned long u4;
typedef unsigned int ud;
typedef unsigned long long u8;
typedef signed char i1;
typedef short i2;
typedef long i4;
typedef int id;
typedef long long i8;
typedef float f4;
typedef double f8;
#define usizeof(a) ((ud)sizeof(a))
#define LOOPS 25000000
#include <stdio.h>
#include <windows.h>
#ifndef bool
typedef signed char bool;
#endif
u8 CPU_rdtsc (void)
{
ud tickl, tickh;
__asm__ __volatile__("rdtsc":"=a"(tickl),"=d"(tickh));
return ((u8)tickh << 32)|tickl;
}
volatile u8 bus_lock (volatile u8 * block, u8 value)
{
__asm__ __volatile__( "xchgq %1,%0" : "=r" (value) : "m" (*block), "0" (value) : "memory");
return value;
}
void bus_unlock (volatile u8 * block, u8 value)
{
__asm__ __volatile__( "movq %0,%1" : "=r" (value) : "m" (*block), "0" (value) : "memory");
}
void rfence (void)
{
__asm__ __volatile__( "lfence" : : : "memory");
}
void rwfence (void)
{
__asm__ __volatile__( "mfence" : : : "memory");
}
void wfence (void)
{
__asm__ __volatile__( "sfence" : : : "memory");
}
volatile bool LOCK_spinlockPreTestIfFree (const volatile u8 *lockVariablePointer)
{
return (bool)(*lockVariablePointer == 0ull);
}
volatile bool LOCK_spinlockFailed (volatile u8 *lockVariablePointer)
{
return (bool)(bus_lock (lockVariablePointer, 1ull) != 0ull);
}
void LOCK_spinlockLeave (volatile u8 *lockVariablePointer)
{
*lockVariablePointer = 0ull;
}
static volatile u8 lockVariable = 0ull,
lockCounter = 0ull;
static volatile i8 threadHold = 1;
static u8 tstr[4][32]; /* 32*8=256 bytes for each thread's parameters should result in them residing in different cache lines */
struct LOCKING_THREAD_STRUCTURE
{
u8 numberOfFailures, numberOfPreTests;
f8 clocksPerLock, failuresPerLock, preTestsPerLock;
u8 threadId;
HANDLE threadHandle;
ud idx;
} *lts[4] = {(void *)tstr[0], (void *)tstr[1], (void *)tstr[2], (void *)tstr[3]};
DWORD WINAPI locking_thread (struct LOCKING_THREAD_STRUCTURE *ltsp)
{
ud n = LOOPS;
u8 clockCycles;
SetThreadAffinityMask (ltsp->threadHandle, 1ull<<ltsp->idx);
while (threadHold) {}
clockCycles = CPU_rdtsc ();
while (n)
{
Sleep (0);
#ifdef CLASSIC_BUS_LOCK
while (LOCK_spinlockFailed (&lockVariable)) {++ltsp->numberOfFailures;}
#else
#ifdef WHILE_PRETEST
while (1)
{
do
{
++ltsp->numberOfPreTests;
} while (!LOCK_spinlockPreTestIfFree (&lockVariable));
if (!LOCK_spinlockFailed (&lockVariable)) break;
++ltsp->numberOfFailures;
}
#else
while (1)
{
++ltsp->numberOfPreTests;
if (LOCK_spinlockPreTestIfFree (&lockVariable))
{
if (!LOCK_spinlockFailed (&lockVariable)) break;
++ltsp->numberOfFailures;
}
}
#endif
#endif
++lockCounter;
LOCK_spinlockLeave (&lockVariable);
#ifdef CLASSIC_BUS_LOCK
while (LOCK_spinlockFailed (&lockVariable)) {++ltsp->numberOfFailures;}
#else
#ifdef WHILE_PRETEST
while (1)
{
do
{
++ltsp->numberOfPreTests;
} while (!LOCK_spinlockPreTestIfFree (&lockVariable));
if (!LOCK_spinlockFailed (&lockVariable)) break;
++ltsp->numberOfFailures;
}
#else
while (1)
{
++ltsp->numberOfPreTests;
if (LOCK_spinlockPreTestIfFree (&lockVariable))
{
if (!LOCK_spinlockFailed (&lockVariable)) break;
++ltsp->numberOfFailures;
}
}
#endif
#endif
--lockCounter;
LOCK_spinlockLeave (&lockVariable);
n-=2;
}
clockCycles = CPU_rdtsc ()-clockCycles;
ltsp->clocksPerLock = (f8)clockCycles/ (f8)LOOPS;
ltsp->failuresPerLock = (f8)ltsp->numberOfFailures/(f8)LOOPS;
ltsp->preTestsPerLock = (f8)ltsp->numberOfPreTests/(f8)LOOPS;
//rwfence ();
ltsp->idx = 4u;
ExitThread (0);
return 0;
}
int main (int argc, char *argv[])
{
u8 processAffinityMask, systemAffinityMask;
memset (tstr, 0u, usizeof(tstr));
lts[0]->idx = 3;
lts[1]->idx = 2;
lts[2]->idx = 1;
lts[3]->idx = 0;
GetProcessAffinityMask (GetCurrentProcess(), &processAffinityMask, &systemAffinityMask);
SetPriorityClass (GetCurrentProcess(), HIGH_PRIORITY_CLASS);
SetThreadAffinityMask (GetCurrentThread (), 1ull);
lts[0]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[0], 0, (void *)<s[0]->threadId);
#ifndef SINGLE_THREAD
lts[1]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[1], 0, (void *)<s[1]->threadId);
lts[2]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[2], 0, (void *)<s[2]->threadId);
lts[3]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[3], 0, (void *)<s[3]->threadId);
#endif
SetThreadAffinityMask (GetCurrentThread (), processAffinityMask);
threadHold = 0;
#ifdef SINGLE_THREAD
while (lts[0]->idx<4u) {Sleep (1);}
#else
while (lts[0]->idx+lts[1]->idx+lts[2]->idx+lts[3]->idx<16u) {Sleep (1);}
#endif
printf ("T0:%1.1f,%1.1f,%1.1f\n", lts[0]->clocksPerLock, lts[0]->failuresPerLock, lts[0]->preTestsPerLock);
printf ("T1:%1.1f,%1.1f,%1.1f\n", lts[1]->clocksPerLock, lts[1]->failuresPerLock, lts[1]->preTestsPerLock);
printf ("T2:%1.1f,%1.1f,%1.1f\n", lts[2]->clocksPerLock, lts[2]->failuresPerLock, lts[2]->preTestsPerLock);
printf ("T3:%1.1f,%1.1f,%1.1f\n", lts[3]->clocksPerLock, lts[3]->failuresPerLock, lts[3]->preTestsPerLock);
printf ("T*:%1.1f,%1.1f,%1.1f\n", (lts[0]->clocksPerLock+ lts[1]->clocksPerLock+ lts[2]->clocksPerLock+ lts[3]->clocksPerLock)/ 4.,
(lts[0]->failuresPerLock+lts[1]->failuresPerLock+lts[2]->failuresPerLock+lts[3]->failuresPerLock)/4.,
(lts[0]->preTestsPerLock+lts[1]->preTestsPerLock+lts[2]->preTestsPerLock+lts[3]->preTestsPerLock)/4.);
printf ("LC:%u\n", (ud)lockCounter);
return 0;
}
该程序在基于DELL i5-4310U的计算机上运行,该计算机具有DDR3-800,2核/ 2 HT,能够达到2.7GHz,并具有通用L3缓存。
首先看来,WOW64的影响可以忽略不计。
执行无竞争锁定/解锁的单个线程能够每110个周期执行一次。调整无竞争锁是没用的:为增强单个XCHG指令而添加的任何代码只会使它变慢。
当四个HT用锁定尝试轰击锁变量时,情况会发生根本变化。实现成功锁定所需的时间跳转到994个周期,其中很大一部分可归因于2.2失败的锁定尝试。换句话说,在高争用的情况下,必须尝试3.2锁才能实现锁定成功。显然110个周期没有变成110 * 3.2但接近110 * 9。所以其他机制在这里起作用就像旧机器上的测试一样。而且,平均994个循环包括716和1157之间的范围
实施预测试的锁定变体需要最简单的变体(XCHG)所需的约95%的循环。平均而言,他们将执行17次CMP,以发现尝试1.75锁定是可行的,其中1次成功。我建议使用预测试不仅因为它更快:它对总线锁定机制施加的压力更小(3.2-1.75 = 1.45减少锁定尝试),即使它稍微增加了复杂性。
答案 2 :(得分:5)
Wikipedia上有一篇关于自旋锁的好文章,这里是x86实现
http://en.wikipedia.org/wiki/Spinlock#Example_implementation
请注意,他们的实施并没有使用&#34;锁定&#34;前缀,因为它在x86上对于&#34; xchg&#34;是多余的。指令 - 它隐含地具有锁语义,如Stackoverflow讨论中所讨论的那样:
On a multicore x86, is a LOCK necessary as a prefix to XCHG?
REP:NOP是PAUSE指令的别名,你可以在这里了解更多信息
How does x86 pause instruction work in spinlock *and* can it be used in other scenarios?
关于记忆障碍的问题,这里有你可能想知道的一切
内存障碍:Paul E. McKenney的软件黑客硬件视图
http://irl.cs.ucla.edu/~yingdi/paperreading/whymb.2010.06.07c.pdf
答案 3 :(得分:3)
看看这里: x86 spinlock using cmpxchg
感谢Cory Nelson
__asm{
spin_lock:
xorl %ecx, %ecx
incl %ecx
spin_lock_retry:
xorl %eax, %eax
lock; cmpxchgl %ecx, (lock_addr)
jnz spin_lock_retry
ret
spin_unlock:
movl $0 (lock_addr)
ret
}
另一位消息人士说: http://www.geoffchappell.com/studies/windows/km/cpu/cx8.htm
lock cmpxchg8b qword ptr [esi]
is replaceable with the following sequence
try:
lock bts dword ptr [edi],0
jnb acquired
wait:
test dword ptr [edi],1
je try
pause ; if available
jmp wait
acquired:
cmp eax,[esi]
jne fail
cmp edx,[esi+4]
je exchange
fail:
mov eax,[esi]
mov edx,[esi+4]
jmp done
exchange:
mov [esi],ebx
mov [esi+4],ecx
done:
mov byte ptr [edi],0
以下是关于无锁vs锁实现的讨论: http://newsgroups.derkeiler.com/Archive/Comp/comp.programming.threads/2011-10/msg00009.html
答案 4 :(得分:-1)
只是问:
在深入研究自旋锁和几乎无锁的数据结构之前:
您是否 - 在您的基准测试和应用程序中 - 确保竞争线程可以保证在不同的内核上运行?
如果没有,你最终可能会得到一个在你的开发机器上运行良好的程序,但在现场很难或很难,因为一个线程必须是你的自旋锁的锁定器和解锁器。
为了给你一个数字:在Windows上你有10毫秒的标准时间片。如果你不确定锁定/解锁涉及两个物理线程,你每秒最多会有大约500个锁定/解锁,这个结果将非常 meh