提高OpenMP / SSE并行化效果

时间:2013-07-26 10:58:56

标签: c++ optimization parallel-processing openmp sse

我试图通过OpenMP(并行)和SSE内在函数提高某些例程的性能:

void Tester::ProcessParallel()//ProcessParallel is member of Tester class
{
    //Initialize
    auto OutMapLen      = this->_OutMapLen;
    auto KernelBatchLen = this->_KernelBatchLen;
    auto OutMapHeig     = this->_OutMapHeig;
    auto OutMapWid      = this->_OutMapWid;
    auto InpMapWid      = this->_InpMapWid;
    auto NumInputMaps   = this->_NumInputMaps;
    auto InpMapLen      = this->_InpMapLen;
    auto KernelLen      = this->_KernelLen;
    auto KernelHeig     = this->_KernelHeig;
    auto KernelWid      = this->_KernelWid;
    auto input_local    = this->input;
    auto output_local   = this->output;
    auto weights_local  = this->weights;
    auto biases_local   = this->biases;
    auto klim           = this->_klim;

    #pragma omp parallel for firstprivate(OutMapLen,KernelBatchLen,OutMapHeig,OutMapWid,InpMapWid,NumInputMaps,InpMapLen,KernelLen,KernelHeig,KernelWid,input_local,output_local,weights_local,biases_local,klim)
    for(auto i=0; i<_NumOutMaps; ++i)
    {   
        auto output_map   = output_local  + i*OutMapLen;
        auto kernel_batch = weights_local + i*KernelBatchLen;
        auto bias = biases_local + i;
        for(auto j=0; j<OutMapHeig; ++j)
        {
            auto output_map_row = output_map + j*OutMapWid;
            auto inp_row_idx = j*InpMapWid;
            for(auto k=0; k<OutMapWid; ++k)
            {
                auto output_nn = output_map_row + k;
                *output_nn     = *bias;
                auto inp_cursor_idx = inp_row_idx + k;
                for(int _i=0; _i<NumInputMaps; ++_i)
                {
                    auto input_cursor = input_local + _i*InpMapLen + inp_cursor_idx;
                    auto kernel = kernel_batch + _i*KernelLen;
                    for(int _j=0; _j<KernelHeig; ++_j)
                    {
                        auto kernel_row_idx  = _j*KernelWid;
                        auto inp_row_cur_idx = _j*InpMapWid;
                        int _k=0;
                        for(; _k<klim; _k+=4)//unroll and vectorize
                        {
                            float buf;
                            __m128 wgt = _mm_loadu_ps(kernel+kernel_row_idx+_k);
                            __m128 inp = _mm_loadu_ps(input_cursor+inp_row_cur_idx+_k);
                            __m128 prd = _mm_dp_ps(wgt, inp, 0xf1);
                            _mm_store_ss(&buf, prd);
                            *output_nn += buf;
                        }
                        for(; _k<KernelWid; ++_k)//residual loop
                            *output_nn += *(kernel+kernel_row_idx+_k) * *(input_cursor+inp_row_cur_idx+_k);
                    }
                }
            }
        }
    }
}

最后一个嵌套循环的纯展开和SSE矢量化(没有OpenMP)提高了总体性能~1.3倍 - 这是非常好的结果。然而,外部环路的纯OpenMP并行化(无需展开/矢量化)在8核处理器(核心i7 2600K)上仅提供~2.1性能增益。总的来说,SSE矢量化和OpenMP parallel_for都表现出2.3-2.7倍的性能提升。如何在上面的代码中提升OpenMP并行化效果?

有趣的是:如果替换“klim”变量 - 在展开最后一个循环时绑定 - 使用标量常量,比如4,总性能增益上升到3.5。

1 个答案:

答案 0 :(得分:1)

在大多数情况下,矢量化和线程不能正交地工作(关于加速计算),即它们的加速不一定加起来。更糟糕的是,这种情况主要发生在像你这样的情况下,数据以流媒体方式处理。原因很简单 - 有限的内存带宽。对是否是这种情况的一个非常简单的衡量标准是所谓的计算强度(CI),定义为在一个字节的输入数据上执行的数据处理量(通常在FLOPS中)。在您的情况下,您加载两个XMM寄存器,总共产生32个字节的数据,然后执行一个点积运算。让我们的代码在2 GHz Sandy Bridge CPU上运行。尽管DPPS在SNB上完成12个周期,但CPU能够重叠几个这样的指令并且每2个周期退出一个。因此,在2 GHz时,每个内核可以在紧密的环路中每秒执行10亿个点产品。它需要32 GB / s的内存带宽来保持这样的循环忙碌。您的情况下所需的实际带宽较少,因为循环中有其他指令,但仍然主要思想仍然存在 - 循环的处理速率受到内存能够馈送到核心的数据量的限制。只要所有数据都适合最后一级缓存(LLC),性能就会随着线程数的增加或多或少而缩放,因为LLC通常会提供相当高的带宽(例如Xeon 7500上的300 GB / s,如所述{{3 }})。一旦数据变得足够大以至于不适合高速缓存,则不是这种情况,因为主存储器通常为每个存储器控制器提供一个数量级更少的带宽。在后一种情况下,所有内核必须共享有限的内存速度,一旦饱和,添加更多线程不会导致加速增加。仅添加更多带宽,例如拥有一个带有多个CPU插槽的系统,可以提高处理速度。

有一种理论模型,称为 Roofline模型,它以更正式的方式捕获它。您可以在here中看到该模型的一些解释和应用。

底线是:矢量化和多处理(例如线程)都会提高性能,但也会增加内存压力。只要内存带宽不饱和,两者都会导致处理速度提高。一旦内存成为瓶颈,性能就不会再增加。甚至有些情况下多线程性能因矢量化带来的额外压力而下降。

可能是优化提示:*output_nn的商店可能无法优化,因为output_nn最终指向共享变量。因此,您可以尝试以下方式:

for(auto k=0; k<OutMapWid; ++k)
{
    auto output_nn = output_map_row + k;
    auto _output_nn = *bias;
    auto inp_cursor_idx = inp_row_idx + k;
    for(int _i=0; _i<NumInputMaps; ++_i)
    {
        ...
        for(int _j=0; _j<KernelHeig; ++_j)
        {
            ...
            for(; _k<klim; _k+=4)//unroll and vectorize
            {
                ...
                _output_nn += buf;
            }
            for(; _k<KernelWid; ++_k)//residual loop
                _output_nn += *(kernel+kernel_row_idx+_k) * *(input_cursor+inp_row_cur_idx+_k);
        }
    }
    *output_nn = _output_nn;
}

但我想你的编译器足够智能,可以自己解决它。无论如何,这只在单线程情况下才有意义。一旦进入饱和内存带宽区域,就不会有这样的优化。