天天看點

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

高斯卷積核具有可分離的性質,是以可以通過以下方法計算二維高斯卷積:構造一個一維高斯卷積核,将原始二維矩陣分别以行主序與列主序,與一維卷積核做卷積計算,得到的結果就是目标二維高斯卷積的結果。本篇按照上述描述的思路實作了可分離的二維高斯卷積計算,并在此基礎上對計算的過程分解與重構,挖掘實作的并行性。

基線版二維高斯卷積

為了讓運作時間更加穩定,增加函數的執行次數至1000

這裡對代碼實作功能做一下簡單的說明。原始矩陣次元432 * 432,次元定義為x和y。高斯卷積核次元31 * 31。基于《 性能優化系列(CPU)——3D高斯核卷積計算(三)FMA向量化計算一維卷積 》中向量化實作的一維卷積的計算函數,先以x方向,按照行主序計算一維高斯卷積;在計算y方向的一維卷積時,由于y方向資料在記憶體中不連續,是以無法複用之前的函數,這裡按照卷積定義式,取y方向上對應的元素,計算點積并儲存。此外,計算過程中需要使用到一個與原始資料尺寸相同的記憶體塊儲存臨時資料。

代碼實作

void Conv2D_Naive(float* pSrc, int iDim[2], float* pKernel, int iKernelSize, float* pTmp)
	{

		for (int y = 0; y < iDim[1]; y++)
		{
			float* pSrcLine = pSrc + y * iDim[0];
			float* pDstLine = pTmp + y * iDim[0];
			Conv1D_opt1(pSrcLine, iDim[0], pKernel, iKernelSize, pDstLine);
		}
		
		int iHalfKernel = iKernelSize / 2;
		memset(pSrc, 0, sizeof(float) * iDim[0] * iDim[1]);
		for (int x = 0; x < iDim[0]; x++)
		{
			for (int y = 0; y < iDim[1] - iKernelSize + 1; y++)
			{
				float fTemp = 0.0f;
				for (int kx = 0, ny = y; kx < iKernelSize; kx++, ny++)
				{
					fTemp += pTmp[ny * iDim[0] + x] * pKernel[kx];
				}
				float* pD = pSrc + (y + iHalfKernel) * iDim[0] + x;
				pSrc[(y + iHalfKernel)*iDim[0] + x] = fTemp;
			}
		}

	}
           

執行時間

TestConv2D(Conv2D_Naive) cost total Time(ms) 1456
TestConv2D cost Time(ms) 1.456
           

VTune分析基線版性能瓶頸

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

可以看到程式整體上記憶體通路與向量化都存在問題。

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

指令執行了90億次

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

記憶體通路方面:L1 Cache Miss 為性能瓶頸。因為做y方向卷積時,以列主序通路不連續的記憶體,記憶體通路模式導緻目标位址不在緩存中,發生miss。

VTune關于L1 Bound的描述:

This metric shows how often machine was stalled without missing the L1 data cache. The L1 cache typically has the shortest latency. However, in certain cases like loads blocked on older stores, a load might suffer a high latency even though it is being satisfied by the L1. Note that this metric value may be highlighted due to DTLB Overhead or Cycles of 1 Port Utilized issues.

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

因為通路不連續的記憶體,向量化程度也受到影響。

VTune關于Cycles of 1 Post Utilized 的描述:

This metric represents cycles fraction where the CPU executed total of 1 uop per cycle on all execution ports. This can be due to heavy data-dependency among software instructions, or oversubscribing a particular hardware resource. In some other cases with high 1_Port_Utilized and L1 Bound, this metric can point to L1 data-cache latency bottleneck that may not necessarily manifest with complete execution starvation (due to the short L1 latency e.g. walking a linked list) - looking at the assembly can be helpful. Note that this metric value may be highlighted due to L1 Bound issue.

分析具體熱點

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

根據之前的描述,在做y方向的卷積時,點積運算是最大的熱點。

檢視點積運算這行的反彙編

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

這裡ICC編譯器對程式通過gather和fma指令,實作了向量化,其中fma指令執行了17億次。本人的了解是:ICC根據代碼實作邏輯,分析出記憶體通路模式,使用gather指令将y方向上的資料合并成一個向量寄存器ymm的長度,進行向量化計算。

關于指令vgatherdps的解釋如下

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

優化版二維卷積

優化版代碼實作邏輯:x方向上一維卷積過程不變,y方向上對計算過程分解,計算過程是将一行資料與對應卷積核的一個點計算并累加更新到目标位置。

代碼實作

void Conv2D_Opt(float* pSrc, int iDim[2], float* pKernel, int iKernelSize, float* pTmp)
	{
		for (int y = 0; y < iDim[1]; y++)
		{
			float* pSrcLine = pSrc + y * iDim[0];
			float* pDstLine = pTmp + y * iDim[0];
			Conv1D_opt1(pSrcLine, iDim[0], pKernel, iKernelSize, pDstLine);
		}

		int iHalfKernel = iKernelSize / 2;
		memset(pSrc, 0, sizeof(float) * iDim[0] * iDim[1]);
		for (int y = 0; y < (iDim[1] - iKernelSize + 1); y++)
		{
			float* pDstLine = pSrc + (y + iHalfKernel) * iDim[0];
			for (int kx = 0; kx < iKernelSize; kx++)
			{
				float* pSrcLine = pTmp + (y + kx) * iDim[0];
#pragma omp simd aligned(pSrcLine, pDstLine)
				for (int i = 0; i < iDim[0]; i++)
				{
					pDstLine[i] += pSrcLine[i] * pKernel[kx];
				}
			}
		}

	}
           

執行速度提高了一倍多

TestConv2D(Conv2D_Opt) cost total Time(ms) 693
TestConv2D cost Time(ms) 0.693
           

VTune分析優化版性能瓶頸

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

從整體執行情況看,記憶體通路的問題已經不存在了。

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

總指令減少了20億次,CPI從0.646降低到0.408

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

和基線版本對比,熱點代碼(y方向的計算)CPI下降到了0.391.

檢視反彙編

C++性能優化系列——3D高斯核卷積計算(五)2D卷積分離計算

這裡vfmadd213ps指令一共隻執行了不到9億次,CPI大概在0.4左右,

對比基線版本vfmadd231ps指令執行17億次,CPI大概1.0。可以看到改變記憶體通路方式後,編譯器對fma的指令做出了調整,向量化指令fma對記憶體資料的依賴更低,在執行數量和執行速度上都得到了優化。

總結

本文基于卷積的定義式,對可分離的高斯卷積進行實作,并分析了存在的性能問題。在此基礎上對計算過程進行重組,最終獲得了兩倍的加速倍數。

繼續閱讀