tbb::parallel_reduce vs tbb::combinable vs tbb::enumerable_t

tbb::parallel_reduce vs tbb::combinable vs tbb::enumerable_thread_specific

本文关键字:tbb vs enumerable reduce parallel combinable      更新时间:2023-10-16

我想浏览一张图像,并处理一些关于元素顺序的特定值。图像有一个包含掩码的unsigned char*数组(如果要处理像素,则为255,否则为0)和一个包含像素值的unsigned short*数组。

我用tbb实现了三种不同的方法,并通过掩码数组使用了一个for循环,并从循环变量x = i%width; y = i/width;计算了x,y坐标。如果像素是可见的,我想用Eigen变换点。vector4d是存储点的std::vector<std::array<double,4>>

下面是我使用tbb的三个实现:

1。tbb::combinabletbb::parallel_for:

void Combinable(int width, int height, unsigned char* mask,unsigned short*  pixel){ 
    MyCombinableType.clear();
    MyCombinableType.local().reserve(width*height);
    tbb::parallel_for( tbb::blocked_range<int>(0, width*height),
        [&](const tbb::blocked_range<int> &r) 
    {       
        vector4d& local = MyCombinableType.local(); 
        const size_t end = r.end(); 
        for (int i = r.begin(); i != end; ++i)
        {
            if(mask[i]!=0)
            {                                       
                array4d arr = {i%width,i/width,(double)pixel[i],1}; 
                //Map with Eigen and transform
                local.push_back(arr);           
            }
        }
    });
    vector4d idx = MyCombinableType.combine(
        []( vector4d x, vector4d y) 
    {               
        std::size_t n = x.size();
        x.resize(n + y.size());
        std::move(y.begin(), y.end(), x.begin() + n);
        return x;
    });
}

2。tbb::enumerable_thread_specifictbb::parallel_for:

void Enumerable(int width, int height, unsigned char* mask,unsigned short*  pixel){
    MyEnumerableType.clear();
    MyEnumerableType.local().reserve(width*height);
    tbb::parallel_for( tbb::blocked_range<int>(0, width*height),
        [&](const tbb::blocked_range<int> &r) 
    {
        enumerableType::reference local = MyEnumerableType.local();
        for (int i = r.begin(); i != r.end(); ++i)
        {
            if(mask[i]!=0)
            {
                array4d arr = {i%width,i/width,(double)pixel[i],1}; 
                //Map with Eigen and transform
                local.push_back(arr);               
            }
        }
    });
    vector4d idx = MyEnumerableType.combine(
        [](vector4d x, vector4d y) 
    {           
        std::size_t n = x.size();
        x.resize(n + y.size());
        std::move(y.begin(), y.end(), x.begin() + n);
        return x;
    });
}

3。tbb::parallel_reduce:

void Reduce(int width, int height, unsigned char* mask,unsigned short*  pixel){
    vector4d idx = tbb::parallel_reduce(
        tbb::blocked_range<int>(0, width*height ),vector4d(),
            [&](const tbb::blocked_range<int>& r, vector4d init)->vector4d 
        {
            const size_t end = r.end(); 
            init.reserve(r.size());
            for( int i=r.begin(); i!=end; ++i )
            {   
                if(mask[i]!=0)
                {               
                    array4d arr = {i%width,i/width,(double)pixel[i],1}; 
                    //Map with Eigen and transform
                    init.push_back(arr);            
                }
            }
            return init;
        },
        []( vector4d x,vector4d y )
        {
            std::size_t n = x.size();
            x.resize(n + y.size());
            std::move(y.begin(), y.end(), x.begin() + n);           
            return x;
        }
    );  
}

我用串行实现比较了三个版本的运行时。数组有8400000个元素,每个算法重复100次。结果如下:

  • 系列:~ 170 ms
  • 可列举的:~ 118 ms
  • 可以化合的:~ 116 ms
  • 减少:~ 720 ms

我假设combine语句是这里的瓶颈。我做错了什么?为什么parallel_reduce这么慢?请帮助!

您可以在这里应用一些优化。

  1. 避免过度复制:传递const vector4d&代替,使用[&] lambda无处不在
  2. 在堆栈上使用临时vector4d,而不是调整其中一个参数的大小,并将其用于返回语句。
  3. 一般使用blocked_range2d,不计算x = i%width; y = i/width。这不仅优化了过多的计算,而且更重要的是,它优化了缓存访问模式,这可能会提高缓存使用率(尽管不是在这种情况下)。

如果您正在使用parallel_reduce的函数形式,请尝试更有效的命令式形式。不幸的是,它不能使用lambdas调用,您必须定义Body类:

https://www.threadingbuildingblocks.org/docs/help/reference/algorithms/parallel_reduce_func.html

它应该最小化在您的还原过程中生成的vector4d副本的数量。vector4d应该是你的Body类的成员,这样它就可以被重用和附加到多个范围,而不是为每个细分的范围构建和合并一个唯一的vector4d。

(注意:分割构造函数不应该复制vector4d成员的内容,注意上面intel的例子中value总是初始化为0)