从多个并行线程读取STL容器是安全的。但是,表现很糟糕。为什么呢?
我创建了一个小对象,它在multiset中存储了一些数据。这使得构造函数相当昂贵(在我的机器上大约有5个usecs。)我在大型多集中存储了数十万个小对象。处理这些对象是一项独立的业务,因此我在多核机器上运行的线程之间拆分工作。每个线程从大型多集合中读取所需的对象,并对其进行处理。
问题是来自大型多重集的读取并不是并行进行的。看起来一个线程中的读取会阻塞另一个线程中的读取。
下面的代码是我能做到的最简单的代码,但仍然显示问题。首先,它创建一个包含100,000个小对象的大型多重集,每个小对象都包含自己的空多集。然后它会串行两次调用multiset复制构造函数,然后再并行调用两次。
分析工具显示串行拷贝构造函数大约需要0.23秒,而并行分析构建器需要两倍的时间。不知何故,并行副本互相干扰。
// a trivial class with a significant ctor and ability to populate an associative container
class cTest
{
multiset<int> mine;
int id;
public:
cTest( int i ) : id( i ) {}
bool operator<(const cTest& o) const { return id < o.id; }
};
// add 100,000 objects to multiset
void Populate( multiset<cTest>& m )
{
for( int k = 0; k < 100000; k++ )
{
m.insert(cTest(k));
}
}
// copy construct multiset, called from mainline
void Copy( const multiset<cTest>& m )
{
cRavenProfile profile("copy_main");
multiset<cTest> copy( m );
}
// copy construct multiset, called from thread
void Copy2( const multiset<cTest>& m )
{
cRavenProfile profile("copy_thread");
multiset<cTest> copy( m );
}
int _tmain(int argc, _TCHAR* argv[])
{
cRavenProfile profile("test");
profile.Start();
multiset<cTest> master;
Populate( master );
// two calls to copy ctor from mainline
Copy( master );
Copy( master );
// call copy ctor in parrallel
boost::thread* pt1 = new boost::thread( boost::bind( Copy2, master ));
boost::thread* pt2 = new boost::thread( boost::bind( Copy2, master ));
pt1->join();
pt2->join();
// display profiler results
cRavenProfile print_profile;
return 0;
}
这是输出
Scope Calls Mean (secs) Total
copy_thread 2 0.472498 0.944997
copy_main 2 0.233529 0.467058
答案 0 :(得分:10)
您提到了复制构造函数。我假设这些也从堆中分配内存?
在多个线程中分配堆内存是大错误。
标准分配器可能是单个池锁定实现。您需要不使用堆内存(堆栈分配),或者需要线程优化堆分配器。
答案 1 :(得分:2)
好的,在这个问题上花了一周的大部分时间后,我有了解决方法。
我在问题中发布的代码存在两个问题:
boost :: bind生成其参数的副本,即使底层函数使用引用调用。复制容器很昂贵,因此多线程版本的工作太难了。 (没有人注意到这一点!)要通过引用传递容器,我需要使用此代码:
boost :: thread * pt1 = new boost :: thread(boost :: bind(Copy2,boost :: cref(master)));
正如Zan Lynx指出的那样,默认容器使用线程安全的单例内存分配器为全局堆上的内容分配内存,导致线程之间的争用很大,因为它们通过同一个分配器创建了数十万个对象实例。 (由于这是神秘的关键,我接受了Zan Lynx的回答。)
如上所述,#1的修正很简单。
正如几位人士指出的那样,对#2的修复是用一个特定于线程的替换默认STL分配器。这是一个非常大的挑战,没有人为这样的分配器提供特定的来源。
我花了一些时间寻找一个“现成的”特定于线程的分配器。我找到的最好的是囤积(hoard.org)。这提供了显着的性能改进,但是囤积有一些严重的缺点
所以我决定基于boost :: pool和boost :: threadspecificptr来推送我自己的特定于线程的内存分配器。这需要少量的恕我直言,非常先进的C ++代码,但现在似乎运作良好。
答案 2 :(得分:0)
您的主题的日程安排是什么?如果你运行两个线程,做了相当多的工作,线程很可能一次启动并立即结束。因此,分析器认为每个线程的执行花费了两倍的时间,因为在每个线程执行工作期间完成两次 。而每个顺序调用的执行都需要正常时间。
step 0 1 2 3 4 5 6 7 8 9
threaded: 1,2,1,2,1,2,1,2,1,2
sequential: 1,1,1,1,1,2,2,2,2,2
线程1从0开始到8结束,执行时间为8;线程2从1开始并在9结束,执行时间为8.两个连续运行每个显示5个步骤。因此,在结果表中,您将看到16个用于并发版本,10个用于顺序版本。
假设以上所有情况都属实且步骤相当多,则探查器显示的执行时间比例应约为2。实验与此假设并不矛盾。
答案 3 :(得分:0)
由于我不确定你的探查器是如何工作的,因此很难说清楚
我希望看到的是围绕代码的明确时间:
然后做几次工作以平均导致上下文切换的任何事情。
for(int loop=0;loop < 100;++loop)
{
ts = timer();
Copy( master );
Copy( master );
te = timer();
tt += te - ts;
}
tt /= 100;
等 将其与您的探查器结果进行比较。
答案 4 :(得分:0)
要回答Pavel Shved的详细信息,以下是我的大部分代码的运行方式:
step 0 1 2 3 4 5 6 7 8 9
core1: 1 1 1 1 1
core2: 2,2,2,2,2
sequential: 1,1,1,1,1,2,2,2,2,2
只有并行读取相互干扰。
作为一项实验,我用一系列指向cTest的指针替换了大型多重集。代码现在有巨大的内存泄漏,但没关系。有趣的是,相对性能更差 - 并行运行复制构造函数会使它们减速4次!
Scope Calls Mean (secs) Total
copy_array_thread 2 0.454432 0.908864
copy_array_main 2 0.116905 0.233811