C++性能优化系列——3D高斯核卷积计算(十一)优化内存访问模式

在上一篇 C++性能优化系列——3D高斯核卷积计算(十)合并多线程并行区 中,存在内存访问问题,本片尝试对内存问题进行调优。

代码实现

之前的实现在计算 Z维度的卷积时,最内层循环逻辑上一共迭代了一个Slice(432 * 432)次,同时,对一个Slice重复做写操作31次。当开辟并行区时,这种实现方式会带来问题:
1.逻辑上需要写内存的内容执行效率不高
2.开辟并行区后,多个核心同时访问内存,造成内存性能瓶颈
这里尝试对上述问题进行优化。从之前的实现逻辑上可以看到,可以尝试将一个内存块的写操作抽离出来,放在更内层执行,这样可以写内存可以更好的利用缓存一致性。同时,根据之前的经验,要考虑ICC 编译器对不同长度的循环的向量化实现的边界处理问题,这里通过试验,最内层循环处理256个float执行速度比较好。此外,对并行区线程束调整,测试下来16线程执行速度更快。
代码实现如下:

void GaussSmoothCPU3DOptZYXSplitZCalc(float* pSrc, int iDim[3], float* pKernel, int kernelSize[3], float* pDst, float* pBuffer)
	{
		//计算结果正确
		//16 thread dynamic GaussSmoothCPU3DOptZYXSplitZCalc cost Time(ms) 148.133
		int iSliceSize = iDim[1] * iDim[0];
		int nCenter = kernelSize[0] / 2;
		const unsigned int InnerSize = 32*8;
#pragma omp parallel for num_threads(16) 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 iSliceIt = 0; iSliceIt < iSliceSize; iSliceIt += InnerSize)
				{
					float* pSrcSlice = pSrc + z * iSliceSize + iSliceIt;
					for (unsigned int kx = 0; kx < kernelSize[0]; kx++)
					{
						float* pSrcPart = pSrcSlice + kx * iSliceSize;
#pragma omp simd aligned(pBuffSlice,pSrcPart)
						for (unsigned int i = 0; i < InnerSize; ++i)
						{
							pBuffSlice[iSliceIt + i] += pSrcPart[i] * 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);
		}

	}

执行时间

GaussSmoothCPU3DOptZYXSplitZCalc cost Time(ms) 148.133

对比上一个版本执行时间提高了35% 左右。

VTune分析性能问题

在这里插入图片描述
整体性能评估上,对比之前的版本访问L3 Cache 的延迟问题得到了改善。
在这里插入图片描述
查看线程执行情况,因为使用了超线程,在线程执行过程中有一定的等待是可以接受的。
在这里插入图片描述

在这里插入图片描述

因为使用了16个线程,因此在总的执行指令数量上比之前版本略多。同时,CPI也变大。
在这里插入图片描述
定位具体的热点代码,依然是计算Z 维度的卷积部分。这里CPI 对比上一个版本增大。
在这里插入图片描述
查看反汇编,这里编译器处理 256 bit向量化时做了8倍循环展开,此处只截取部分汇编实现。从汇编代码可以看到ICC编译器在优化后的汇编指令有两点让人看不懂:
1.最内层循环做了8倍循环展开,同时对卷积核的同一个点的broadcast操作也执行了8次,逻辑上只做一次就可以了
2.获取写内存地址的指针时CPI指标高,而实际执行写操作时的CPI反而没有太高,这里暂时没搞清楚为什么获取指针操作会这么慢

总结

本篇尝试调整Z维度卷积计算时的内层计算顺序来优化内存访问问题。从执行速度来看,性能得到提升。但同时也发现两个本人解释不了的问题。暂时先将问题记录下来,待后续查阅相关资料后进行解释。

补充

关于问题
1.最内层循环做了8倍循环展开,同时对卷积核的同一个点的broadcast操作也执行了8次,逻辑上只做一次就可以了
对broadcast指令和fma指令的执行次数进行统计,比例大概是1:8。因此上文描述的内层循环broadcast执行8次是不准确的,应该是VTune显示反汇编重复显示导致理解偏差。
2.获取写内存地址的指针时CPI指标高,而实际执行写操作时的CPI反而没有太高,这里暂时没搞清楚为什么获取指针操作会这么慢
参考Intel 手册解释:
LEA can be dispatched via port 1 and 5 in most cases, doubling the throughput over prior generations.
However this apply only to LEA instructions with one or two source operands.
For LEA instructions with three source operands and some specific situations, instruction latency has increased to 3 cycles, and must dispatch via port 1:
LEA that uses base and index registers where the base is EBP, RBP, or R13.
大致意思就是使用RBP寄存器指令CPI会升高到3.处理这个问题只能从汇编Level对程序进行调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值