2

我试图通过 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。

4

1 回答 1

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)。一旦数据增长到无法放入缓存中,情况就不是这样了,因为主内存通常为每个内存控制器提供的带宽要少一个数量级。在后一种情况下,所有内核必须共享有限的内存速度,一旦饱和,添加更多线程不会导致加速增加。只有增加更多带宽,例如让系统具有多个 CPU 插槽,才能提高处理速度。

有一个理论模型,称为Roofline 模型,它以更正式的方式捕捉到这一点。您可以在此演示文稿中看到该模型的一些解释和应用。

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

可能是一个优化提示:由于最终指向共享变量内部,因此存储*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;
}

但我猜你的编译器足够聪明,可以自己计算出来。无论如何,这仅在单线程情况下很重要。一旦进入饱和内存带宽区域,就没有这样的优化了。

于 2013-07-26T17:04:42.660 回答