cmpxchg for WORD比BYTE快

时间:2012-08-15 21:39:16

标签: c++ multithreading assembly inline-assembly

昨天我发布了this question关于如何编写快速自旋锁的信息。感谢Cory Nelson,我似乎找到了一个比我的问题中讨论的其他方法更优越的方法。我使用CMPXCHG指令来检查锁是否为0并因此是空闲的。 CMPXCHG在'BYTE',WORDDWORD上运行。我认为该指令在BYTE上运行得更快。但我写了一个实现每种数据类型的锁:

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
    }
}

然后使用以下伪代码测试锁(请注意,lcm指针始终指向可被4分割的地址):

<int/short/char>* lck;
threadFunc()
{
    loop 10,000,000 times
    {
        spin_lock_8/16/32 (lck);
        spin_unlock(lck);
    }
}
main()
{
    lck = (char/short/int*)_aligned_malloc(4, 4);//Ensures memory alignment
    start 1 thread running threadFunc and measure time;
    start 2 threads running threadFunc and measure time;
    start 4 threads running threadFunc and measure time;
    _aligned_free(lck);
}

我已经在具有2个物理内核的处理器上以msecs测量了以下结果,可以运行4个线程(Ivy Bridge)。

           1 thread    2 threads     4 threads
8-bit      200         700           3200
16-bit     200         500           1400
32-bit     200         900           3400

数据表明所有功能都需要相同的时间来执行。但是当多个线程必须检查使用16位的lck == 0是否会明显更快。这是为什么?我不认为它与lck

的对齐有关

提前致谢。

2 个答案:

答案 0 :(得分:2)

想象一下,有1234个线程和16个CPU。一个线程获取自旋锁,然后操作系统执行任务切换。现在你有16个CPU,每个运行剩余的1233个线程中的一个,所有这些都以非常无意义的方式旋转,因为操作系统将CPU时间提供给可以释放自旋锁的唯一线程。这意味着整个操作系统基本上可以锁定(所有CPU平稳)几秒钟。这是严重迟钝的;那你怎么解决它?

您通过不在用户空间中使用自旋锁来修复它。只有当/可以禁用任务开关时,才应使用自旋锁;并且只有内核应该能够禁用任务切换。

更具体地说,您需要使用互斥锁。现在,互斥体可能会在放弃之前最初旋转并使线程等待锁定,并且(对于典型/低争用情况)这确实有帮助,但它仍然是互斥锁并且不是自旋锁。

下一步;对于理智的软件,重要的(性能)是避免锁争用,然后确保无争用的情况是快的(如果没有争用,好的互斥锁不会导致任务切换)。您正在测量争用/无关的案例。

最后;你的锁很糟糕。为了避免过度使用lock前缀,您应该测试是否可以在没有任何lock前缀的情况下获取,并且只有在您可以获得时才应使用lock前缀。英特尔(可能还有许多其他人)称这种策略为“测试;然后(测试和设置)”。此外,你还没有理解pause(或“rep nop”对于那些非常糟糕且不支持10年前指令的汇编程序的目的)。

半个不错的自旋锁可能看起来像:

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; dr; 您不应该使用错误的工具开始工作(自旋锁);但如果你坚持使用错误的工具,那么至少要正确实施错误的工具......: - )

答案 1 :(得分:1)

从我记得锁定在一个单词(2个字节)上工作。它是在第一次引入486时以这种方式编写的。

如果你携带一个不同大小的锁,它实际上产生相当于2个锁(锁定字A和字B为双字)。对于一个字节,它可能必须防止锁定第二个字节,有点类似于2个锁...

因此,您的结果与CPU优化一致。