在上一篇 C++性能优化系列——3D高斯核卷积计算(八)3D高斯卷积 中,按照 X Y Z的顺序,依次计算每个维度的一维卷积。其代码实现中一个开辟了两个并行区,其中计算X Y 维度开辟一个并行区,计算Z 维度开辟一个并行区。本片基于可分离卷积的性质,按照 Z Y X 的顺序,计算每个维度的一维卷积。这样处理的可以将两个并行区合并成一个并行区,减少了一个并行区同步的操作。
代码实现
这里简单阐述一下实现思路:Z 维度的计算依次提取原始数据31层数据,并将每一层分别与卷积核的一个元素相乘,再将每一层的计算结果相加,得到目标结果(一层数据)。接下来,对这一层数据的X Y方向的计算通过复用之前实现的二维高斯卷积函数。
代码实现
void GaussSmoothCPU3DOptZYX(float* pSrc, int iDim[3], float* pKernel, int kernelSize[3], float* pDst, float* pBuffer)
{
int iSliceSize = iDim[1] * iDim[0];
int nCenter = kernelSize[0] / 2;
#pragma omp parallel for num_threads(8) schedule(dynamic)
for (int z = 0; z < (iDim[2] - kernelSize[0] + 1); z++)
{
float* pBuffSlice = pBuffer + (z + nCenter) * iSliceSize;
float* pDstSlice = pDst + (z + nCenter) * iSliceSize;
for (unsigned int kx = 0; kx < kernelSize[0]; kx++)
{
float* pSrcSlice = pSrc + (z + kx) * iSliceSize;
#pragma omp simd
for (unsigned int iSliceIt = 0; iSliceIt < iSliceSize; ++iSliceIt)
{
pBuffSlice[iSliceIt] += pSrcSlice[iSliceIt] * pKernel[kx];
}
}
int tid = omp_get_thread_num();
memset(yBuf[tid], 0, sizeof(float) * iDim[0] * iDim[1]);
Conv2D_Fuse(pBuffSlice, iDim, pKernel, kernelSize[0],yBuf[tid] , pDstSlice, NULL);
}
}
执行时间
16线程 dynamic调度
GaussSmoothCPU3DOptZYX cost Time(ms) 338.4
16线程 static调度
GaussSmoothCPU3DOptZYX cost Time(ms) 590
8线程 dynamic调度
GaussSmoothCPU3DOptZYX cost Time(ms) 230.9
从执行时间上可以看到:一个并行区通过使用超线程,速度并没有得到提升。
VTune分析性能问题
总体执行情况如下
说明:这里为了使执行时间稳定,重复调用函数30次。
性能瓶颈主要是访问L3 Cache与内存加载。
定位热点语句:计算Z 维度的乘加运算。
反馈的性能问题与程序的整体问题相对应。计算时每个线程在一次for迭代中分别要加载 432 * 432 *4 * 31 * 2 Byte 的数据,写432 * 432 * 4 * 31 Byte的数据,虽然连续访问内存,但是对同一块内存(432 * 432 * 4 Byte)写31次。访问内存模式导致内存性能瓶颈。
查看反汇编,这里编译器对循环做了2倍展开,CPI高的指令是fmadd 和movup的操作。这里fmadd一个操作数是从内存中加载,movup是将计算的结果写回内存。与前文分析的内容基本一致。
总结
本片通过改变三维卷积的计算维度顺序,将两个并行区合并成一个并行区。但同时,目前的实现方式存在明显的内存访问问题。该问题将在后续篇章中修复。