堆上的多线程(de)分配

Phl*_*ous 6 c++ qt multithreading memory-management

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

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

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

搜索SO表明在不同线程上分配和解除分配是安全的.减速的来源是什么,我该怎么办呢?

编辑:一些示例代码传达了正在发生的事情: 为了排除故障,我已经从这个示例中完全删除了磁盘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);
Run Code Online (Sandbox Code Playgroud)

以下是有和没有向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进一步加剧了.

dte*_*ech 7

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

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

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

  • @Phlucious - 只有计算可以从多线程中受益.因此,使用更多线程进行内存管理只会导致性能受到重大影响,因为不同的线程正在争夺同步.将新的或自定义分配器与内存池放在一起可以显着减少分配数量,并将多个对象放入一大块内存中.这样可以显着节省CPU时间和内存使用量.最重要的是,由于数据碎片较少,因此它可能对缓存更友好. (2认同)

120*_*arm 2

(更新评论以回答)

这可能是因为对于一个线程,所有分配都是连续的,因此释放也是连续的。对于多线程分配,它们更加混合,因此每次释放后 free 需要做更多的工作来清理。