C++性能优化系列——3D高斯核卷积计算(八)3D高斯卷积

本篇基于可分离卷积的性质,按照 X Y Z的顺序,依次计算每个维度的一维卷积。

代码实现

因为是按照X Y Z的计算顺序,因此只能够在计算X维度的卷积时,复用之前实现的一维卷积计算函数。Y维度的计算是将一个Z平面上的二维数据中每行与卷积核中一个点相乘,并将31个点的卷积核计算出的结果累加至一行,更新到中间缓存的目标位置。Z维度的计算是将一个Z平面的二维数据和卷积核中的一个点相乘,并将31个点的卷积核计算出的结果累加至一个二维平面,更新到结果的目标位置。这里对Y 和 Z维度的计算都是通过编译器ICC实现向量化。
代码实现如下:

void GaussSmoothCPU3DBase(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;
		for (int z = 0; z < (iDim[2]); z++)
		{
			float* pSrcSlice = pSrc + z * iSliceSize;
			float* pBuffSlice = pBuffer + z * iSliceSize;
			memset(pBuffSlice, 0, iSliceSize * sizeof(float));
			for (int y = 0; y < iDim[1]; y++)
			{
				float* pSrcLine = pSrcSlice + y * iDim[0];
				float* pDstLine = pBuffSlice + y * iDim[0];
				Conv1D_Opt_Cmb(pSrcLine, iDim[0], pKernel, kernelSize[0], pDstLine);
			}
			for (int y = 0; y < (iDim[1] - kernelSize[0] + 1); y++)
			{
				float* pDstLine = pSrcSlice + (y + nCenter) * iDim[0];
				memset(pDstLine, 0, iDim[0] * sizeof(float));
				for (int kx = 0; kx < kernelSize[0]; kx++)
				{
					float* pSrcLine = pBuffSlice + (y + kx) * iDim[0];
#pragma omp simd
					for (int i = 0; i < iDim[0]; i++)
					{
						pDstLine[i] += pSrcLine[i] * pKernel[kx];
					}
				}
			}
		}

		for (int z = 0; z < (iDim[2] - kernelSize[0] + 1); z++)
		{
			float* pDstSlice = pDst + (z + nCenter) * iSliceSize;
			memset(pDstSlice, 0, iSliceSize * sizeof(float));
			for (int kx = 0; kx < kernelSize[0]; kx++)
			{
				float* pSrcSlice = pSrc + (z + kx) * iSliceSize;
#pragma omp simd
				for (int i = 0; i < iSliceSize; ++i)
				{
					pDstSlice[i] += pKernel[kx] * pSrcSlice[i];
				}
			}
		}
	}

执行时间

GaussSmoothCPU3DBase cost Time(ms) 1010.5

多线程并行化

计算逻辑不变,基于OpemMP实现多线程并行化。因此线程数设置8与16分别测试其执行耗时情况,并选择速度最快的版本。
代码实现如下:

void GaussSmoothCPU3DBase(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(16) schedule(dynamic)
		for (int z = 0; z < (iDim[2]); z++)
		{
			float* pSrcSlice = pSrc + z * iSliceSize;
			float* pBuffSlice = pBuffer + z * iSliceSize;
			memset(pBuffSlice, 0, iSliceSize * sizeof(float));
			for (int y = 0; y < iDim[1]; y++)
			{
				float* pSrcLine = pSrcSlice + y * iDim[0];
				float* pDstLine = pBuffSlice + y * iDim[0];
				Conv1D_Opt_Cmb(pSrcLine, iDim[0], pKernel, kernelSize[0], pDstLine);
			}
			for (int y = 0; y < (iDim[1] - kernelSize[0] + 1); y++)
			{
				float* pDstLine = pSrcSlice + (y + nCenter) * iDim[0];
				memset(pDstLine, 0, iDim[0] * sizeof(float));
				for (int kx = 0; kx < kernelSize[0]; kx++)
				{
					float* pSrcLine = pBuffSlice + (y + kx) * iDim[0];
#pragma omp simd aligned(pSrcLine, pDstLine)
					for (int i = 0; i < iDim[0]; i++)
					{
						pDstLine[i] += pSrcLine[i] * pKernel[kx];
					}
				}
			}
		}

#pragma omp parallel for num_threads(8) schedule(dynamic)
		for (int z = 0; z < (iDim[2] - kernelSize[0] + 1); z++)
		{
			float* pDstSlice = pDst + (z + nCenter) * iSliceSize;
			memset(pDstSlice, 0, iSliceSize * sizeof(float));
			for (int kx = 0; kx < kernelSize[0]; kx++)
			{
				float* pSrcSlice = pSrc + (z + kx) * iSliceSize;
#pragma omp simd
				for (int i = 0; i < iSliceSize; ++i)
				{
					pDstSlice[i] += pKernel[kx] * pSrcSlice[i];
				}
			}
		}
	}

执行时间

GaussSmoothCPU3DBase cost Time(ms) 218.4

VTune分析性能问题

指令执行情况如下:
在这里插入图片描述
其中,为了执行结果稳定,重复调用函数30次。
线程占用率如下:
在这里插入图片描述
可以看到线程大部分时间还是在做有用工作的。

计算X Y维度卷积的性能状态
在这里插入图片描述
整体上没有突出的性能问题。
在这里插入图片描述
热点语句是Y维度的FMA运算。
在这里插入图片描述
查看反汇编,其中broadcast指令CPI比较高。关于指令的解释如下:
在这里插入图片描述broadcast指令CPI理论值为1。这里抓取的CPI为1.4,略低于理论值。这里执行这个指令的原因是将一维卷积核的一个点展开成一个向量,但是根据反汇编中broadcast指令的执行次数和fmadd是一个数量级的,推断ICC在这里应该是内层循环每次迭代都做了一次broadcast,但显然有更高效的做法:只将卷积核展开一次,并保存在寄存器中复用,效率会更高。
计算Z维度卷积的执行状态
在这里插入图片描述
整体上内存访问为主要的性能问题。
在这里插入图片描述
在Z维度FMA运算时CPI最高。
在这里插入图片描述
查看反汇编指令,其中加载内存与broadcast指令CPI高。
计算X Y维度和计算Z维度过程类似,为什么CPI差距会这么大呢?在内存访问上,XY维度的计算每次迭代,所有内存读写操作都2 * 432 * 432 的内存块内进行;Z维度的计算每次迭代,内存读写跨度在332 * 432 *432 ,因此造成内存访问成为性能瓶颈。由Z维度计算的逻辑设计所限制,每次迭代必须要访问这么多的内存数据,因此目前针对内存访问的问题,还没找到很好的解决方案。

总结

本文按照 X Y Z的维度顺序,实现了3D高斯卷积的计算,同时基于OpenMP技术,实现了多线程并行化。同时分析了Z维度计算时造成内存瓶颈的原因。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值