映射模式在多线程程序中的性能低于预期(4倍加速vs 8倍加速)



我正在开始多线程编程,所以请原谅我,如果下面看起来很明显。我正在将多线程添加到图像处理程序中,并且加速并不完全是我所期望的。

我目前在4个物理处理器的cpu上使用超线程(8)获得了4倍的加速,所以我想知道这种加速是否预期。我唯一能想到的是,如果一个物理CPU的两个超线程必须共享某种内存总线,这可能是有意义的。

作为多线程的新手,我不完全清楚这是否会被认为是一个I/O绑定程序,考虑到所有内存都分配在RAM中(我理解我的操作系统的虚拟内存管理器将决定从堆中调入/调出这个假定的内存数量)我的机器有16Gb的RAM,以防它有助于决定是否分页/交换可以是一个问题。

我用QThreadPool和tbb::parallel_for编写了一个测试程序,展示了串行用例和两个并行用例。

当前的程序,正如你所看到的,除了将假设的图像从黑色设置为白色之外,没有任何实际的操作,并且在对图像应用任何实际操作之前,它是故意知道基线是什么。

我附上了程序,希望有人能解释我,如果我的追求一个大约8倍的加速是一个失败的原因在这种处理算法。请注意,我对其他类型的优化(如SIMD)不感兴趣,因为我真正关心的不仅仅是使它更快,而是使用纯多线程使它更快,而不需要进入SSE或处理器缓存级优化。

#include <iostream>
#include <sys/time.h>
#include <vector>
#include <QThreadPool>
#include "/usr/local/include/tbb/tbb.h"
#define LOG(x) (std::cout << x << std::endl)
struct col4
{
    unsigned char r, g, b, a;
};
class QTileTask : public QRunnable
{
public:
    void run()
    {
        for(uint32_t y = m_yStart; y < m_yEnd; y++)
        {
            int rowStart = y * m_width;
            for(uint32_t x = m_xStart; x < m_xEnd; x++)
            {
                int index = rowStart + x;
                m_pData[index].r = 255;
                m_pData[index].g = 255;
                m_pData[index].b = 255;
                m_pData[index].a = 255;
            }
        }
    }
    col4*          m_pData;
    uint32_t       m_xStart;
    uint32_t       m_yStart;
    uint32_t       m_xEnd;
    uint32_t       m_yEnd;
    uint32_t       m_width;
};
struct TBBTileTask
{
    void operator()()
    {
        for(uint32_t y = m_yStart; y < m_yEnd; y++)
        {
            int rowStart = y * m_width;
            for(uint32_t x = m_xStart; x < m_xEnd; x++)
            {
                int index = rowStart + x;
                m_pData[index].r = 255;
                m_pData[index].g = 255;
                m_pData[index].b = 255;
                m_pData[index].a = 255;
            }
        }
    }
    col4*          m_pData;
    uint32_t       m_xStart;
    uint32_t       m_yStart;
    uint32_t       m_xEnd;
    uint32_t       m_yEnd;
    uint32_t       m_width;
};
struct TBBCaller
{
    TBBCaller(std::vector<TBBTileTask>& t)
        : m_tasks(t)
    {}
    TBBCaller(TBBCaller& e, tbb::split)
        : m_tasks(e.m_tasks)
    {}
    void operator()(const tbb::blocked_range<size_t>& r) const
    {
        for (size_t i=r.begin();i!=r.end();++i)
            m_tasks[i]();
    }
    std::vector<TBBTileTask>& m_tasks;
};
inline double getcurrenttime( void )
{
    timeval t;
    gettimeofday(&t, NULL);
    return static_cast<double>(t.tv_sec)+(static_cast<double>(t.tv_usec) / 1000000.0);
}
char* getCmdOption(char ** begin, char ** end, const std::string & option)
{
    char ** itr = std::find(begin, end, option);
    if (itr != end && ++itr != end)
    {
        return *itr;
    }
    return 0;
}
bool cmdOptionExists(char** begin, char** end, const std::string& option)
{
    return std::find(begin, end, option) != end;
}
void baselineSerial(col4* pData, int resolution)
{
    double t = getcurrenttime();
    for(int y = 0; y < resolution; y++)
    {
        int rowStart = y * resolution;
        for(int x = 0; x < resolution; x++)
        {
            int index = rowStart + x;
            pData[index].r = 255;
            pData[index].g = 255;
            pData[index].b = 255;
            pData[index].a = 255;
        }
    }
    LOG((getcurrenttime() - t) * 1000 << " ms. (Serial)");
}
void baselineParallelQt(col4* pData, int resolution, uint32_t tileSize)
{
    double t = getcurrenttime();
    QThreadPool pool;
    for(int y = 0; y < resolution; y+=tileSize)
    {
        for(int x = 0; x < resolution; x+=tileSize)
        {
            uint32_t xEnd = std::min<uint32_t>(x+tileSize, resolution);
            uint32_t yEnd = std::min<uint32_t>(y+tileSize, resolution);
            QTileTask* t = new QTileTask;
            t->m_pData = pData;
            t->m_xStart = x;
            t->m_yStart = y;
            t->m_xEnd = xEnd;
            t->m_yEnd = yEnd;
            t->m_width = resolution;
            pool.start(t);
        }
    }
    pool.waitForDone();
    LOG((getcurrenttime() - t) * 1000 << " ms. (QThreadPool)");
}
void baselineParallelTBB(col4* pData, int resolution, uint32_t tileSize)
{
    double t = getcurrenttime();
    std::vector<TBBTileTask> tasks;
    for(int y = 0; y < resolution; y+=tileSize)
    {
        for(int x = 0; x < resolution; x+=tileSize)
        {
            uint32_t xEnd = std::min<uint32_t>(x+tileSize, resolution);
            uint32_t yEnd = std::min<uint32_t>(y+tileSize, resolution);
            TBBTileTask t;
            t.m_pData = pData;
            t.m_xStart = x;
            t.m_yStart = y;
            t.m_xEnd = xEnd;
            t.m_yEnd = yEnd;
            t.m_width = resolution;
            tasks.push_back(t);
        }
    }
    TBBCaller caller(tasks);
    tbb::task_scheduler_init init;
    tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), caller);
    LOG((getcurrenttime() - t) * 1000 << " ms. (TBB)");
}
int main(int argc, char** argv)
{
    int resolution = 1;
    uint32_t tileSize = 64;
    char * pResText = getCmdOption(argv, argv + argc, "-r");
    if (pResText)
    {
        resolution = atoi(pResText);
    }
    char * pTileSizeChr = getCmdOption(argv, argv + argc, "-b");
    if (pTileSizeChr)
    {
        tileSize = atoi(pTileSizeChr);
    }
    if(resolution > 16)
        resolution = 16;
    resolution = resolution << 10;
    uint32_t tileCount = resolution/tileSize + 1;
    tileCount *= tileCount;
    LOG("Resolution: " << resolution << " Tile Size: "<< tileSize);
    LOG("Tile Count: " << tileCount);
    uint64_t pixelCount = resolution*resolution;
    col4* pData = new col4[pixelCount];
    memset(pData, 0, sizeof(col4)*pixelCount);
    baselineSerial(pData, resolution);
    memset(pData, 0, sizeof(col4)*pixelCount);
    baselineParallelQt(pData, resolution, tileSize);
    memset(pData, 0, sizeof(col4)*pixelCount);
    baselineParallelTBB(pData, resolution, tileSize);
    delete[] pData;
    return 0;
}

是的,预期4倍的加速。超线程是一种在硬件上实现的分时,所以如果一个线程用光了核心上所有可用的超标量管道,您就不能指望从中受益,就像您的情况一样。其他线程必须等待。

如果您的内存总线带宽被在少于可用内核总数的情况下运行的线程饱和,则可以预期更低的加速。通常情况下,如果你有太多的核心,就像这个问题:

为什么这段代码不是线性扩展的?

最新更新