堆上的多线程(de)分配

时间:2016-12-20 18:01:50

标签: c++ multithreading qt memory-management

我有一个非常大的~30M对象,每个大约80字节 - 对于那些跟随的人来说是~2.2GB - 存储在磁盘上。每个对象的实际大小略有不同,因为每个对象都有QMap<quint32, QVariant>个孩子。

从原始数据中解压缩这些对象是很昂贵的,所以我实现了一个多线程读取操作,从磁盘顺序拉出几MB,然后将每个原始数据块传递给一个线程以解压缩并行通过QtConcurrent。我的对象在工作线程内的堆上创建(通过new),然后传递回主线程以进行下一步。完成后,将在主线程上删除这些对象。

在单线程环境中,这种释放相对较快(约4-5秒)。但是,当在4个线程上进行多线程处理时,这种释放速度非常慢(约26-36秒)。使用Very Sleepy进行分析表明减速是在MSVCR100 free中,因此释放本身很慢。

搜索SO表示allocating and deallocating on different threads is safe。减速的来源是什么,我该怎么办呢?

编辑:一些示例代码传达了正在发生的事情: 为了排除故障,我已从此示例中完全删除了磁盘IO,只需创建对象然后将其删除。

class MyObject
{
public:
    MyObject() { /* set defaults... irrelevant here */}
    ~MyObject() {}
    QMap<quint32, QVariant> map;
    //...other members
}

//...

QList<MyObject*> results;

/* set up the mapped lambda functor (QtConcurrent reqs std::function if returning) */
std::function<QList<MyObject*>(quint64 chunksize)>
        importMap = [](quint64 chunksize) -> QList<MyObject*>
{
    QList<MyObject*> objs;
    for(int i = 0; i < chunksize; ++i)
    {
        MyObject* obj = new MyObject();
        obj->map.insert(0, 1);      //ran with and without the map insertions
        obj->map.insert(1, 2);
        objs.append(obj);
    }
    return objs;
}; //end import map lambda

/* set up the reduce lambda functor */
auto importReduce = [&results](bool& /*noreturn*/, const QList<MyObject*> chunkimported)
{
    results.append(chunkimported);
}; //end import reduce lambda

/* chunk up the data for import */
quint64 totalcount = 31833986;
quint64 chunksize = 500000;
QList<quint64> chunklist;
while(totalcount >= chunksize)
{
    totalcount -= chunksize;
    chunklist.append(chunksize);
}
if(totalcount > 0)
    chunklist.append(totalcount);

/* create the objects concurrently */
QThreadPool::globalInstance()->setMaxThreadCount(1);    //4 for multithreaded run
QElapsedTimer tnew; tnew.start();
QtConcurrent::mappedReduced<bool>(chunklist, importMap, importReduce, QtConcurrent::OrderedReduce | QtConcurrent::SequentialReduce);
qDebug("DONE NEW %f", double(tnew.elapsed())/1000.0);

//do stuff with the objects here

/* delete the objects */
QElapsedTimer tdelete; tdelete.start();
qDeleteAll(results);
qDebug("DONE DELETE %f", double(tdelete.elapsed())/1000.0);

以下是有和没有向MyObject :: map插入数据的结果,以及QtConcurrent可用的1或4个线程:

  • 1个主题:tnew = 2.7秒; tdelete = 1.1秒
  • 4个主题:tnew = 1.8秒; tdelete = 2.7秒
  • 1个线程+ QMap:tnew = 8.6秒; tdelete = 4.6秒
  • 4个主题+ QMap:tnew = 4.0秒; tdelete = 48.1秒

在这两种情况下,当在4个线程上并行创建对象时,删除对象需要更长的时间,而在1个线程上是串行创建的,并且通过并行插入QMap进一步加剧了这些对象。

5 个答案:

答案 0 :(得分:7)

这几乎是猜测,但我认为操作系统内存管理器将是一个系统范围,毕竟它为一个虚拟内存池提供服务,因此抛出更多线程不会提高速度,它只会阻塞它高架。线程安全加上并发访问总是会受到惩罚。所以你投入的线程越多,你获得的惩罚就越多。

30M分配是相当多的,无论分配的大小如何,它也代表了显着的开销内存消耗。我建议您花时间实现内存池,预先分配整块内存,并使用placement new来分配这些池中的对象。这将是一个巨大的CPU节省时间和显着的内存保护。此外,它还可以通过减少碎片来提高缓存友好性和缓存命中率。

把它作为一个比喻,将4个厨师放在一个炉子上不会使烹饪速度提高4倍,这将使每个厨师至少慢4倍加上浪费时间与资源使用冲突。这几乎就是你在实践中所看到的。

答案 1 :(得分:2)

(更新评论以回答)

这可能是因为一个线程的所有分配都是顺序的,所以frees也是如此。通过多线程分配,它们可以更加混合,因此每次释放后都需要做更多工作来清理。

答案 2 :(得分:1)

当从多个线程分配单个内存池时,您将在重新分配期间创建瓶颈,因为按顺序删除的单元是不相邻的。

如果您使用固定大小分配,您应该能够在分配器/ dealloctor中将其用于O(1)类型性能。一个单元分配系统,将一堆相同大小的块放入一个空闲列表,然后根据需要推送/弹出它们是你应该研究的。

答案 3 :(得分:1)

已知内存分配和释放速度很慢,操作系统正在对内存的访问进行排序。这种排序使新的和免费的线程安全,但也大大减慢了事情。

通常的做法是,如果每个片段都是固定大小,则预先分配大块内存。

另一种方法是使用内存映射文件来绕过分配。 Qt具有内存映射文件类,可以在所有平台上使用。 你可以尝试这种方法, How do you serialize a QMap?

答案 4 :(得分:0)

我很想为每个线程分配一个相对较大的内存块,并且在该线程内尝试使用它,就好像它是一个堆栈(或作为循环缓冲区)。如果您总是在一端放置新对象并从另一端删除它们,这可能很有效。或者,如果您可以在一个步骤中取消分配一组对象(就像函数调用返回时堆栈一样)。否则,您确实需要从堆中获取的新功能和删除功能,正如您所发现的那样,在某些情况下可能会成为主要的性能瓶颈。

编辑:我认为我们错过了这一点。你最后的删除速度是如此之慢,这真的没有意义。如果我在那时正确理解了代码,那么你只能运行主线程吗?