使用Intel线程构建块(TBB)的Parallel_Scan组件无法实现加速

No Speedup achieved using Parallel_Scan Component of Intel Thread Building Blocks (TBB)

本文关键字:组件 Scan 加速 实现 Parallel 构建 线程 Intel TBB 使用      更新时间:2023-10-16

我正在探索Intel线程构建块中的Parallel_Scan组件,这是在关联操作的情况下使用的,我发现Parallel_Scan比它在串行中完成的要多10倍。

我写的检查代码是:

#include <iostream>
#include <stdlib.h>
#include <time.h>
#include "tbb/task_scheduler_init.h"
#include "tbb/blocked_range.h"
#include "tbb/parallel_scan.h"
#include "tbb/tick_count.h"
using namespace std;
using namespace tbb;
template <class T>
class Body 
{
    T reduced_result;
    T* const y;
    const T* const x;
    public:
    Body( T y_[], const T x_[] ) : reduced_result(0), x(x_), y(y_) {}
    T get_reduced_result() const {return reduced_result;}
    template<typename Tag>
    void operator()( const blocked_range<int>& r, Tag ) 
    {
        T temp = reduced_result;
        for( int i=r.begin(); i<r.end(); ++i ) 
        {
            temp = temp+x[i];
            if( Tag::is_final_scan() )
            y[i] = temp;
        }
        reduced_result = temp;
    }
    Body( Body& b, split ) : x(b.x), y(b.y), reduced_result(10) {}
    void reverse_join( Body& a ) 
    {
        reduced_result = a.reduced_result + reduced_result;
    }
    void assign( Body& b ) 
    {   
        reduced_result = b.reduced_result;
    }
};

template<class T>
float DoParallelScan( T y[], const T x[], int n) 
{
    Body<int> body(y,x);
    tick_count t1,t2,t3,t4;
    t1=tick_count::now();
    parallel_scan( blocked_range<int>(0,n), body , auto_partitioner() );
    t2=tick_count::now();
    cout<<"Time Taken for parallel scan is t"<<(t2-t1).seconds()<<endl;
    return body.get_reduced_result();
}

template<class T1>
float SerialScan(T1 y[], const T1 x[], int n)
{
    tick_count t3,t4;
    t3=tick_count::now();
    T1 temp = 10;
    for( int i=1; i<n; ++i ) 
    {
        temp = temp+x[i];
        y[i] = temp;
    }
    t4=tick_count::now();
    cout<<"Time Taken for serial  scan is t"<<(t4-t3).seconds()<<endl;
    return temp;
}

int main()
{
    task_scheduler_init init1;
    int y1[100000],x1[100000];
    for(int i=0;i<100000;i++)
        x1[i]=i;
    cout<<fixed;
    cout<<"n serial scan output is t"<<SerialScan(y1,x1,100000)<<endl;
    cout<<"n parallel scan output is t"<<DoParallelScan(y1,x1,100000)<<endl;
    return 0;
} 

请帮我找出我错在哪里

我是tbb::parallel_scan的原作者。

在使用"大核"的多核系统上获得并行扫描的加速可能很难。原因是并行扫描本质上是一个两步算法。如果数据不适合外部缓存,并行扫描必须从内存中传入数据两次,而串行算法只需要做一次。对于像整数加法这样简单的操作,内存流量(而不是ALU)通常是"大核心"的瓶颈,因为"大核心"将大量硬件资源用于快速串行执行。如果数据适合外部缓存,则可能没有足够的工作来分摊并行开销。

我能够通过以下更改和条件为您的示例获得一些并行加速(约2x):

  • 我在循环之前将r.r end()的读提升到一个局部变量中,如下所示:

    int rend = r.end();
    for( int i=r.begin(); i<rend; ++i )
    

    这样做有助于编译器生成更好的代码,因为它知道rend是循环不变的。如果没有提升,编译器必须假设对y[i]的写入可能会覆盖r.end()读取的r字段。类似地,将字段x和y的读取提升到局部变量中可能会有所帮助,尽管编译器应该能够从基于类型的别名消歧中判断对y[i]的写入不会影响这些字段。

  • 我将输入数组增加到有10,000,000个元素,因此有更多的工作要做,从而更好地分摊并行调度开销。为了避免堆栈溢出,我在堆中分配了数组。

  • 我预热了TBB运行时。一般来说,在进行这种计时练习时,最好先进行一次"丢弃"运行,这样就不会计算一次性启动成本。为了预热(对于串行和并行代码),我在时序逻辑周围包装了一个三次循环,如下所示:

    for( int k=0; k<3; ++k ) {
        cout<<"n serial scan output is t"<<SerialScan(y1,x1,n)<<endl;
        cout<<"n parallel scan output is t"<<DoParallelScan(y1,x1,n)<<endl;
    }
    

    这是我在大多数计时实验中所做的事情,所以我可以看到首次成本是否显著,或者是否存在其他兴趣变化。

  • 我用"gcc -O2 -ltbb"编译。

  • 我在一个16核系统上运行,有两个"Sandy Bridge"芯片。

查看内存带宽影响的一种方法是将示例中的T更改为较小的类型。当我编辑示例将T从int改为char(从而将内存带宽需求减少约4倍)时,并行加速增加了。(注:示例中有一个"Body"应该是"Body")

查看内存带宽影响的另一种方法是在具有许多小内核的系统上尝试这个示例。我在英特尔Xeon Phi处理器(TM)上尝试了这个例子,并根据前面描述的类型进行了修改,该处理器具有高内存带宽和许多小内核。我可以得到4 -7倍的平行加速。将问题大小提高到100,000,000,我的速度提高了10 -20倍。

总而言之:只有当并行计算的好处超过两次传递数据的开销时,多线程扫描才能获得回报。

总共需要多长时间?也许你的输入太小了,因此上下文切换主导了并行版本的运行时间。如果你增加习题集,它会有什么表现?如果你做一些计算量更大的事情比如你现在做的简单求和,它会有什么表现?