【问题标题】:boost::thread data structure sizes on the ridiculous side?boost::thread 数据结构的大小在荒谬的一边?
【发布时间】:2011-10-12 14:04:53
【问题描述】:

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

我已经有一段时间没有编写任何复杂的低级系统代码了,我通常针对系统原语(windows 和 pthreads/posix)进行编程。所以,in#s和out的已经从我的记忆中溜走了。我目前正在与boost::asioboost::thread 合作。

为了模拟针对异步函数执行器的同步 RPC(boost::io_service 具有多个线程 io::service::run'ing,其中请求是 io_serviced::post'ed),我使用了 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

40 字节的互斥锁 ?? ?? ?哇!条件变量为 88 !!!请记住,我对这种臃肿的大小感到厌恶,因为我正在考虑一个可以创建数百个 notification_object 的应用程序

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

【问题讨论】:

  • 你怎么能解释这些类型是“臃肿”的可移植性,而不是例如。功能?
  • 这很可能是,但是,从文档来看,功能并没有超出系统特定库允许您做的事情。如果您认为这是由于功能问题,请为它提出论据作为答案:D

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


【解决方案1】:

当您查看任何类型的同步原语的“大小开销”时,请记住这些不能过于紧密地打包。之所以如此,是因为例如如果两个互斥锁同时使用,则共享一个缓存行最终会导致缓存垃圾(错误共享),即使获取这些锁的用户从不“冲突”。 IE。想象两个线程运行两个循环:

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

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

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

因此,即使自旋锁或互斥锁基础的原始数据类型的实际数据大小可能只是一个字节或一个 32 位字,但此类的有效数据大小对象通常更大。

在断言“我的互斥体太大”之前请记住这一点。事实上,在 x86/x64 上,40 字节太小无法防止错误共享,因为缓存线目前至少有 64 字节。

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

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

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

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

【讨论】:

  • 哇,这篇文章中有大量的背景信息,我自己永远无法发现。 +1 升船。
  • 可能我看错了你的回答,但是我真的看不出相关性:你自己说,boost::mutex的大小不够,防止虚假分享,所以这显然不是原因。此外,与任何其他对象相比,错误共享与互斥锁的相关性并不高 - 您是否认为任何数据类型都应该至少有 64 个字节来防止这种情况发生?您可以通过在对象之间添加填充字节或通过指定对齐方式来防止虚假共享,而不是通过增加对象的大小。
  • @MikeMB:你把两件事混为一谈了——boost::mutex 的大小not足够大虚假共享可以发生 - 即,当 两个 boost::mutex 实例 [ 至少部分 ] 在同一缓存行中时。您认为“人为增加对象大小”和“在之间添加 [stuff] 字节”之间有什么区别?那是一样的。你做什么取决于你的用例。要紧密封装锁定基元数组,您必须增加大小。要与锁定原语共同托管“受保护”对象,请将两者打包到同一缓存行中。
  • @FrankH.:如果我“人为地增加对象大小”,通过将填充字节添加到数据类型中,sizeof 运算符将返回数据结构的大小,包括额外的填充字节。如果我在两个对象之间添加字节,那么情况并非如此。现在,假设您所说的 有效数据大小sizeof 运算符返回的(这毕竟是 OP 所要求的),事实上,boost::mutex 小于缓存线而不是表示它没有被塞满。
  • 哦,我认为最好不要填充类本身的原因是,这允许用户决定,他是否需要将对象放在单独的缓存行上,即 - 正如你陈述你自己 - 并非总是如此,那么你为什么要拒​​绝你的班级用户的选择呢?
【解决方案2】:

在我的 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 具有额外内部互斥体的开销。后者是否适合您的申请由您决定。

附:我会鼓励你坚持事实,避免在你的帖子中使用煽动性语言。我几乎决定忽略你的帖子,纯粹是因为它的语气。

【讨论】:

  • 在 Windows 上,您在用户代码中看到的变量很可能是指代某个更大数据结构的句柄 - 请务必考虑到这一点。
  • 是的,我想这是有道理的,谢谢。我想我需要调查一下为什么互斥体需要这么多字节。
【解决方案3】:

查看实现:

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 之外,我发现系统互斥体不会出现太多开销。

【讨论】:

  • 而且 noncopyable 符合空基类优化的条件。
  • +1 用于查看源代码,答案显而易见
【解决方案4】:

对不起,我在这里发表评论,但我没有足够的声誉来添加评论。

@FrankH,缓存垃圾并不是使数据结构更大的好理由。有些缓存行甚至可以有 128 字节的大小,但这并不意味着互斥体必须这么大。

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

@Hassan Syed, 我不认为互斥锁是在这种类型的缓存优化中被编程思考的。相反,我认为这是它们为支持优先级继承、嵌套锁等思想而编程的方式。作为建议,如果您的程序中需要大量互斥锁,请考虑使用诸如互斥锁池(数组)之类的东西,并在节点中仅存储一个索引(当然要注意内存分离)。我让你考虑一下这个解决方案的细节。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-16
    • 2017-05-18
    相关资源
    最近更新 更多