在荒谬的一面boost :: thread数据结构大小?

时间:2011-07-25 12:55:10

标签: c++ boost-asio boost-thread micro-optimization systems-programming

编译器:linux上的clang ++ x86-64。

已经有一段时间了,因为我编写了任何复杂的低级系统代码,并且我对系统原语(windows和pthreads / posix)进行了编程。所以,#s和out的内容已经从我的记忆中消失了。我目前正在与boost::asioboost::thread合作。

为了针对异步函数执行器(boost::io_service模拟具有多个线程io::service::run的请求为io_serviced::post')的同步RPC,我使用了boost同步原语。为了好奇,我决定sizeof原语。这就是我所看到的。

struct notification_object
{
  bool ready;
  boost::mutex m;
  boost::condition_variable v;
};
...
std::cout << sizeof(bool) << std::endl;
std::cout << sizeof(boost::mutex) << std::endl;
std::cout << sizeof(boost::condition_variable) << std::endl;
std::cout << sizeof(notification_object) << std::endl;
...

输出:

1
40
88
136

互斥锁的四十个字节? ?? ? WTF! 88为条件_变量!!!请记住,我被这个膨胀的大小击退,因为我正在考虑一个可以创建数百个notification_object的应用程序

这种便携性开销似乎很荒谬,有人可以证明这一点吗?据我所知,这些原语应该是4或8字节宽,具体取决于CPU的内存模型。

4 个答案:

答案 0 :(得分:23)

当您查看任何类型的同步原语的“大小开销”时,请记住这些不能打包过于紧密。这是因为例如如果共享高速缓存行的两个互斥锁最终会被缓存废弃(假共享),如果它们同时使用,即使获取这些锁的用户永远不会“冲突”。即想象两个线程运行两个循环:

for (;;) {
    lock(lockA);
    unlock(lockA);
}

for (;;) {
    lock(lockB);
    unlock(lockB);
}

与运行一个循环的一个线程相比,当在两个不同的线程上运行时,您将看到两倍的迭代次数当且仅当这两个锁不在同一个高速缓存行内时 >。如果lockAlockB位于同一个高速缓存行中,则每个线程的迭代次数将减半 - 因为具有这两个锁的高速缓存行将在执行的cpu核心之间永久反弹这两个线程。

因此,即使自旋锁或互斥锁下的原始数据类型的实际数据大小可能只是一个字节或32位字,有效数据大小一个物体往往更大。

在声明“我的互斥锁太大”之前请记住这一点。实际上,在x86 / x64上,40字节太小以防止错误共享,因为当前的缓存行目前至少有64个字节。

除此之外,如果您高度关注内存使用情况,请考虑通知对象不必是唯一的 - 条件变量可以用于触发不同的事件(通过predicate知道的boost::condition_variable )。因此,可以对整个状态机使用单个互斥/ CV对,而不是每个状态使用一个这样的对。同样适用于例如线程池同步 - 拥有比线程更多的锁定并不一定有益。

编辑:有关“虚假共享”的更多参考资料(以及在同一个高速缓存行中托管多个原子更新的变量所造成的负面性能影响),请参阅(以及其他)以下SO过帐:

如上所述,当在多核,每个内核的缓存配置中使用多个“同步对象”(无论是原子更新的变量,锁,信号量......)时,允许它们中的每一个单独的空间缓存行。你在这里交换内存使用的可扩展性,但实际上,如果你进入你的软件需要几百万个锁的区域(制作那些GB的内存),你要么拥有几百GB内存的资金(和一百个CPU核心),或者你在软件设计中做错了什么。

在大多数情况下(对于class / struct的特定实例的锁/原子),只要包含原子变量的对象实例,就可以免费获得“填充”足够大了。

答案 1 :(得分:19)

在我的64位Ubuntu框中,以下内容:

#include <pthread.h>
#include <stdio.h>

int main() {
  printf("sizeof(pthread_mutex_t)=%ld\n", sizeof(pthread_mutex_t));
  printf("sizeof(pthread_cond_t)=%ld\n", sizeof(pthread_cond_t));
  return 0;
}

打印

sizeof(pthread_mutex_t)=40
sizeof(pthread_cond_t)=48

这表明您声称

  

这种便携性开销似乎很荒谬,可以是个人   向我证明这一点?据我所知,这些原语应该是   宽度为4或8字节,具体取决于CPU的内存型号。

根本不是真的。

如果您想知道boost::condition_variable占用的额外40个字节来自哪里,Boost类使用内部互斥锁。

简而言之,在此平台上,boost::mutexpthread_mutex_t相比具有完全开销,而boost::condition_variable具有额外内部互斥锁的开销。后者是否可以为您的申请所接受,由您决定。

P.S。我鼓励你坚持事实,避免在你的帖子中使用煽动性语言。我一个人几乎决定完全忽视你的帖子。

答案 2 :(得分:6)

查看实施情况:

class mutex : private noncopyable
{
public:
    friend class detail::thread::lock_ops<mutex>;

    typedef detail::thread::scoped_lock<mutex> scoped_lock;

    mutex();
    ~mutex();

private:
#if defined(BOOST_HAS_WINTHREADS)
    typedef void* cv_state;
#elif defined(BOOST_HAS_PTHREADS)
    struct cv_state
    {
        pthread_mutex_t* pmutex;
    };
#elif defined(BOOST_HAS_MPTASKS)
    struct cv_state
    {
    };
#endif
    void do_lock();
    void do_unlock();
    void do_lock(cv_state& state);
    void do_unlock(cv_state& state);

#if defined(BOOST_HAS_WINTHREADS)
    void* m_mutex;
#elif defined(BOOST_HAS_PTHREADS)
    pthread_mutex_t m_mutex;
#elif defined(BOOST_HAS_MPTASKS)
    threads::mac::detail::scoped_critical_region m_mutex;
    threads::mac::detail::scoped_critical_region m_mutex_mutex;
#endif
};

现在,让我去除非数据部分并重新排序:

class mutex : private noncopyable {
private:
#if defined(BOOST_HAS_WINTHREADS)
    void* m_mutex;
#elif defined(BOOST_HAS_PTHREADS)
    pthread_mutex_t m_mutex;
#elif defined(BOOST_HAS_MPTASKS)
    threads::mac::detail::scoped_critical_region m_mutex;
    threads::mac::detail::scoped_critical_region m_mutex_mutex;
#endif
};

除了noncopyable之外,我发现系统互斥体没有出现太大的开销。

答案 3 :(得分:3)

很抱歉,我在这里发表评论,但我没有足够的声誉来添加评论。

@FrankH,缓存垃圾并不是使数据结构更大的充分理由。缓存行甚至可以有128个字节的大小,它并不意味着互斥量必须如此之大。

我认为必须警告程序员将内存中的同步对象分开,这样他们就不会共享同一个缓存行。通过将对象插入足够大的数据结构中可以实现什么,而不会使用未使用的字节膨胀数据结构。另一方面,插入未使用的字节会降低程序速度,因为CPU必须获取更多的高速缓存行才能访问相同的结构。

@Hassan Syed, 我不认为互斥体是在这种类型的缓存优化中进行编程思考的。相反,我认为这是他们编程支持思维的方式,如优先级继承,嵌套锁,.......作为建议,如果你的程序中需要很多互斥锁,可以考虑类似于互斥体的池(数组)并在节点中存储一个索引(当然要处理内存分离)。我让你考虑一下这个解决方案的细节。