【问题标题】:difference between pthread_spinlock and boost::smart_ptr::spinlock?pthread_spinlock 和 boost::smart_ptr::spinlock 的区别?
【发布时间】:2012-06-22 03:03:00
【问题描述】:

我在boost::smart_ptr中找到了以下自旋锁代码:

bool try_lock()
{
    return (__sync_lock_test_and_set(&v_, 1) == 0);
}
void lock()
{    
    for (unsigned k=0; !try_lock(); ++k)
    {
        if (k<4)
            ; // spin
        else if (k < 16)
            __asm__ __volatile__("pause"); // was ("rep; nop" ::: "memory")
        else if (k < 32 || k & 1)
            sched_yield();
        else
        {
            struct timespec rqtp;
            rqtp.tv_sec  = 0;
            rqtp.tv_nsec = 100;
            nanosleep(&rqtp, 0);
        }
    }
}
void unlock()
{
     __sync_lock_release(&v_);
}

所以如果我理解正确的话,当锁被争用时,传入的线程将呈指数级回退,首先疯狂旋转,然后暂停,然后产生其时间片的剩余部分,最后在睡眠和产生之间翻转。

我还找到了glibc pthread_spinlock 实现,它使用汇编来执行锁定。

#define LOCK_PREFIX "lock;" // using an SMP machine

int pthread_spin_lock(pthread_spinlock_t *lock)
{
    __asm__ ("\n"
       "1:\t" LOCK_PREFIX "decl %0\n\t"
       "jne 2f\n\t"
       ".subsection 1\n\t"
       ".align 16\n"
       "2:\trep; nop\n\t"
       "cmpl $0, %0\n\t"
       "jg 1b\n\t"
       "jmp 2b\n\t"
       ".previous"
       : "=m" (*lock)
       : "m" (*lock));

    return 0;
}

我承认我对汇编的理解不是很好,所以我不完全理解这里发生了什么。 (有人可以解释一下这是在做什么吗?)

但是,我对 boost spinlock 和 glibc pthread_spinlock 进行了一些测试,当 内核多于线程时,boost 代码的性能优于 glibc 代码

另一方面,当线程多于内核时,glibc 代码更好

这是为什么?这两种自旋锁实现有什么区别,使它们在每种情况下的表现都不同?

【问题讨论】:

  • 有趣的是,几年前我进行了类似的测试并得出了相同的结论:当有很多争用时,pthread_spin_lock 比手动自旋锁更有效(来自 boost 的那一行)。

标签: c++ linux performance spinlock


【解决方案1】:

您从哪里获得问题中发布的pthread_spin_lock() 实现?它似乎遗漏了几行重要的内容。

我看到的实现(不是内联汇编——它是来自glibc/nptl/sysdeps/i386/pthread_spin_lock.S 的独立汇编源文件)看起来很相似,但有两个额外的关键说明:

#include <lowlevellock.h>

    .globl  pthread_spin_lock
    .type   pthread_spin_lock,@function
    .align  16
pthread_spin_lock:
    mov 4(%esp), %eax
1:  LOCK
    decl    0(%eax)
    jne 2f
    xor %eax, %eax
    ret

    .align  16
2:  rep
    nop
    cmpl    $0, 0(%eax)
    jg  1b
    jmp 2b
    .size   pthread_spin_lock,.-pthread_spin_lock

它减少传入参数指向的long,如果结果为零则返回。

否则,结果为非零,这意味着该线程没有获取锁。所以它执行rep nop,相当于pause指令。这是一个“特殊” nop,它向 CPU 提示线程处于自旋状态,并且 cpu 应该以某种方式处理内存排序和/或分支预测,以提高这些情况下的性能(我不假装准确理解在芯片的保护下发生了什么不同 - 从软件的角度来看,与普通的旧 nop 没有区别。

pause 之后,它会再次检查该值 - 如果它大于零,则锁未被认领,因此它跳到函数的顶部并尝试再次认领锁。否则,它会再次跳转到pause

这个自旋锁和 Boost 版本之间的主要区别在于,这个自旋锁在旋转时从不做任何比 pause 更精彩的事情 - 没有什么比 sched_yield()nanosleep() 更好的了。所以线程保持热。我不确定这在您提到的两种行为中是如何发挥作用的,但是 glibc 代码会更贪婪 - 如果一个线程在锁上旋转并且有其他线程准备运行但没有可用的核心,则旋转线程不会t 帮助等待的线程获得任何 cpu 时间,而 Boost 版本最终会自愿为等待关注的线程让路。

【讨论】:

  • 我在网上某个地方找到了它——不幸的是我记不起具体在哪里——我在测试中使用了 pthread_spin_lock,发现了我报告的结果,当我看到差异时,就去寻找源代码尝试了解发生了什么的代码,找到了该程序集,当我无法理解时,来这里寻求帮助。感谢您的回复和解释!
  • 我想知道这对我来说似乎违反直觉 - 当核心被过度订阅(线程多于核心)时,不放弃其核心竞争的贪婪 pthread 实现表现更好 - 我会预计在这种情况下 boost 实现会做得更好。
  • @lori:很难说发生了什么——我们没有关于基准的信息。也就是说,我不确定在用户模式代码中使用自旋锁的频率有多高。即使他们有时这样做,我认为他们应该只在人们期望低争用并且他们会被尽可能简短地持有时才使用。执行 Boost 所做的检查可能有点过度设计。再说一次,这只是基于直觉的意见,而不是数据。
猜你喜欢
  • 2020-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-25
  • 1970-01-01
  • 2015-01-28
相关资源
最近更新 更多