C++ cmpxchg for WORD快于for BYTE
昨天我发布了关于如何编写快速自旋锁的文章。多亏了Cory Nelson,我似乎找到了一种优于我问题中讨论的其他方法的方法。我使用C++ cmpxchg for WORD快于for BYTE,c++,multithreading,assembly,inline-assembly,C++,Multithreading,Assembly,Inline Assembly,昨天我发布了关于如何编写快速自旋锁的文章。多亏了Cory Nelson,我似乎找到了一种优于我问题中讨论的其他方法的方法。我使用CMPXCHG指令检查锁是否为0,从而释放CMPXCHG对`字节'、WORD和DWORD进行操作。我假设指令在字节上运行得更快。但我编写了一个锁来实现每种数据类型: inline void spin_lock_8(char* lck) { __asm { mov ebx, lck ;move
CMPXCHG
指令检查锁是否为0,从而释放CMPXCHG
对`字节'、WORD
和DWORD
进行操作。我假设指令在字节上运行得更快。但我编写了一个锁来实现每种数据类型:
inline void spin_lock_8(char* lck)
{
__asm
{
mov ebx, lck ;move lck pointer into ebx
xor cl, cl ;set CL to 0
inc cl ;increment CL to 1
pause ;
spin_loop:
xor al, al ;set AL to 0
lock cmpxchg byte ptr [ebx], cl ;compare AL to CL. If equal ZF is set and CL is loaded into address pointed to by ebx
jnz spin_loop ;jump to spin_loop if ZF
}
}
inline void spin_lock_16(short* lck)
{
__asm
{
mov ebx, lck
xor cx, cx
inc cx
pause
spin_loop:
xor ax, ax
lock cmpxchg word ptr [ebx], cx
jnz spin_loop
}
}
inline void spin_lock_32(int* lck)
{
__asm
{
mov ebx, lck
xor ecx, ecx
inc ecx
pause
spin_loop:
xor eax, eax
lock cmpxchg dword ptr [ebx], ecx
jnz spin_loop
}
}
inline spin_unlock(<anyType>* lck)
{
__asm
{
mov ebx, lck
mov <byte/word/dword> ptr [ebx], 0
}
}
数据表明,所有函数的执行时间相等。但是当多个线程必须检查lck==0
时,使用16位可以大大加快速度。为什么呢?我认为这与lck
的对齐无关
提前谢谢。据我回忆,锁可以锁定一个单词(2个字节)。它在486年首次引入时就是这样写的
如果你携带一个不同大小的锁,它实际上会生成相当于两个锁(双字的锁字a和锁字B)。对于一个字节,它可能必须防止第二个字节的锁,这有点类似于两个锁
因此,您的结果与CPU优化一致。假设有1234个线程和16个CPU。一个线程获取自旋锁,然后操作系统进行任务切换。现在,您有16个CPU,每个CPU运行剩余1233个线程中的一个,无论操作系统将CPU时间返回到唯一可以释放自旋锁的线程需要多长时间,所有CPU都以一种毫无意义的方式旋转。这意味着整个操作系统基本上可以锁定(所有CPU都会耗尽)几秒钟。这是严重的迟钝;那么你如何修复它呢
您可以通过在用户空间中不使用旋转锁来修复它。只有在可以禁用任务开关的情况下才能使用自旋锁;只有内核才能禁用任务切换
更具体地说,您需要使用互斥锁。现在,互斥锁在放弃并让线程等待锁之前可能会先旋转,并且(对于典型/低争用情况)这确实有帮助,但它仍然是互斥锁,而不是旋转锁
其次;对于sane软件,重要的(性能)是避免锁争用,然后确保无争用的情况是快速的(如果没有争用,良好的互斥不会导致任务切换)。您正在衡量争用/无关案例
最后,;你的锁坏了。为了避免过度使用lock
前缀,您应该测试您是否能够在没有任何lock
前缀的情况下获取,并且只有当您能够获取时才应该使用lock
前缀。英特尔(可能还有很多其他人)将这种策略称为“测试;然后(测试并设置)”。此外,您还没有理解暂停的目的(或者对于不支持10年前的指令的汇编程序来说,是“rep nop”)
半正常的自旋锁可能看起来像:
acquire:
lock bts dword [myLock],0 ;Optimistically attempt to acquire
jnc .acquired ;It was acquired!
.retry:
pause
cmp dword [myLock],0 ;Should we attempt to acquire again?
jne .retry ; no, don't use `lock`
lock bts dword [myLock],0 ;Attempt to acquire
jc .retry ;It wasn't acquired, so go back to waiting
.acquired:
ret
release:
mov dword [myLock],0 ;No lock prefix needed here as "myLock" is aligned
ret
还要注意的是,如果您未能充分降低锁争用的机会,那么您确实需要关心“公平性”,不应该使用自旋锁。“不公平”自旋锁的问题是,有些任务可能很幸运,并且总是得到锁,而有些任务可能很不幸,并且永远不会得到锁,因为幸运的任务总是得到锁。对于竞争激烈的锁来说,这一直是一个问题,但对于现代NUMA系统来说,这已经成为一个更可能的问题。在这种情况下,您至少应该使用票据锁
票证锁的基本思想是确保任务按照它们到达的顺序(而不是一些“可能非常糟糕”的随机顺序)获得锁。为完整起见,票证锁可能如下所示:
acquire:
mov eax,1
lock xadd [myLock],eax ;myTicket = currentTicket, currentTicket++
cmp [myLock+4],eax ;Is it my turn?
je .acquired ; yes
.retry:
pause
cmp [myLock+4],eax ;Is it my turn?
jne .retry ; no, wait
.acquired:
ret
release:
lock inc dword [myLock+4]
ret
tl;博士一开始,您不应该使用错误的作业工具(旋转锁);但是如果你坚持使用错误的工具,那么至少要正确地实现错误的工具…:-) “我知道这没有多大区别,但由于自旋锁是一个被大量使用的对象”-在30多年的多线程软件开发中没有明确使用过一个。请尝试将pause
指令移动到自旋循环内部,而不是循环外部。16位指令需要额外的0x66/0x67前缀字节,使其比8位或32位指令稍大/稍慢。因此,在16位的情况下,额外的开销可能会减慢循环速度,从而减少争用。如果这些锁导致随机损坏,我不会感到惊讶,因为它们在不保存和还原ebx(被调用方保存寄存器)的情况下修改了ebx,这可能会损坏调用方希望保留的某些值。请改用edx。@ChrisDodd:因为这不是裸汇编方法,所以EBX寄存器由编译器在周围的代码中保存和还原。我强烈建议使用内部函数来执行暂停和cmpxchg之类的操作。内联组装不是一条好路。是的,暂停指令肯定应该在循环中。它会暂停线程10个周期左右(这会提升其他线程的超线程能力),并警告CPU内存排序可能会发生冲突,并且省去了CPU在失效时检测到内存顺序冲突并必须刷新/重新启动指令时必须重新启动的时间。请注意,正确实现互斥锁的唯一方法是使用自旋锁,除非您希望内核仅在执行任务切换时才允许互斥锁(假设所有线程都停止了。)我可以告诉您,在Linux中,互斥锁使用的是自旋锁。
acquire:
lock bts dword [myLock],0 ;Optimistically attempt to acquire
jnc .acquired ;It was acquired!
.retry:
pause
cmp dword [myLock],0 ;Should we attempt to acquire again?
jne .retry ; no, don't use `lock`
lock bts dword [myLock],0 ;Attempt to acquire
jc .retry ;It wasn't acquired, so go back to waiting
.acquired:
ret
release:
mov dword [myLock],0 ;No lock prefix needed here as "myLock" is aligned
ret
acquire:
mov eax,1
lock xadd [myLock],eax ;myTicket = currentTicket, currentTicket++
cmp [myLock+4],eax ;Is it my turn?
je .acquired ; yes
.retry:
pause
cmp [myLock+4],eax ;Is it my turn?
jne .retry ; no, wait
.acquired:
ret
release:
lock inc dword [myLock+4]
ret