C++ cmpxchg for WORD快于for BYTE

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

昨天我发布了关于如何编写快速自旋锁的文章。多亏了Cory Nelson,我似乎找到了一种优于我问题中讨论的其他方法的方法。我使用
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