最近一直沉迷於SSE方面的優化,實在找不到想學習的參考資料了,就拿個筆記本放在腿上翻翻OpenCv的源代碼,無心中看到了OpenCv中關於積分圖的代碼,仔細研習了一番,以爲OpenCv對SSE的靈活運用真的作的很好,這裏記錄下我對該段代碼的品味並將其思路擴展到其餘通道數的圖像。算法
該核心代碼位於:Opencv 3.0\opencv\sources\modules\imgproc\src\sumpixels.cpp文件中。ide
咱們貼出最感興趣的一部分代碼以便分析:函數
bool operator()(const uchar * src, size_t _srcstep,int * sum, size_t _sumstep,double * sqsum, size_t, int * tilted, size_t,Size size, int cn) const { if (sqsum || tilted || cn != 1 || !haveSSE2) return false; // the first iteration memset(sum, 0, (size.width + 1) * sizeof(int)); __m128i v_zero = _mm_setzero_si128(), prev = v_zero; int j = 0; // the others for (int i = 0; i < size.height; ++i) { const uchar * src_row = src + _srcstep * i; int * prev_sum_row = (int *)((uchar *)sum + _sumstep * i) + 1; int * sum_row = (int *)((uchar *)sum + _sumstep * (i + 1)) + 1; sum_row[-1] = 0; prev = v_zero; j = 0; for ( ; j + 7 < size.width; j += 8) { __m128i vsuml = _mm_loadu_si128((const __m128i *)(prev_sum_row + j)); __m128i vsumh = _mm_loadu_si128((const __m128i *)(prev_sum_row + j + 4)); __m128i el8shr0 = _mm_loadl_epi64((const __m128i *)(src_row + j)); __m128i el8shr1 = _mm_slli_si128(el8shr0, 1); __m128i el8shr2 = _mm_slli_si128(el8shr0, 2); __m128i el8shr3 = _mm_slli_si128(el8shr0, 3); vsuml = _mm_add_epi32(vsuml, prev); vsumh = _mm_add_epi32(vsumh, prev); __m128i el8shr12 = _mm_add_epi16(_mm_unpacklo_epi8(el8shr1, v_zero), _mm_unpacklo_epi8(el8shr2, v_zero)); __m128i el8shr03 = _mm_add_epi16(_mm_unpacklo_epi8(el8shr0, v_zero), _mm_unpacklo_epi8(el8shr3, v_zero)); __m128i el8 = _mm_add_epi16(el8shr12, el8shr03); __m128i el4h = _mm_add_epi16(_mm_unpackhi_epi16(el8, v_zero), _mm_unpacklo_epi16(el8, v_zero)); vsuml = _mm_add_epi32(vsuml, _mm_unpacklo_epi16(el8, v_zero)); vsumh = _mm_add_epi32(vsumh, el4h); _mm_storeu_si128((__m128i *)(sum_row + j), vsuml); _mm_storeu_si128((__m128i *)(sum_row + j + 4), vsumh); prev = _mm_add_epi32(prev, _mm_shuffle_epi32(el4h, _MM_SHUFFLE(3, 3, 3, 3))); } for (int v = sum_row[j - 1] - prev_sum_row[j - 1]; j < size.width; ++j) sum_row[j] = (v += src_row[j]) + prev_sum_row[j]; }
爲了說明更方便,這裏貼出我作的普通C語言的代碼和從新優化後的SSE代碼。學習
普通C語言:測試
void GetGrayIntegralImage(unsigned char *Src, int *Integral, int Width, int Height, int Stride) { memset(Integral, 0, (Width + 1) * sizeof(int)); // 第一行都爲0 for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; int *LinePL = Integral + Y * (Width + 1) + 1; // 上一行位置 int *LinePD = Integral + (Y + 1) * (Width + 1) + 1; // 當前位置,注意每行的第一列的值都爲0 LinePD[-1] = 0; // 第一列的值爲0 for (int X = 0, Sum = 0; X < Width; X++) { Sum += LinePS[X]; // 行方向累加 LinePD[X] = LinePL[X] + Sum; // 更新積分圖 } } }
優化後的SSE算法:優化
void GetGrayIntegralImage(unsigned char *Src, int *Integral, int Width, int Height, int Stride) { memset(Integral, 0, (Width + 1) * sizeof(int)); // 第一行都爲0 int BlockSize = 8, Block = Width / BlockSize; for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; int *LinePL = Integral + Y * (Width + 1) + 1; // 上一行位置 int *LinePD = Integral + (Y + 1) * (Width + 1) + 1; // 當前位置,注意每行的第一列的值都爲0 LinePD[-1] = 0; __m128i PreV = _mm_setzero_si128(); __m128i Zero = _mm_setzero_si128(); for (int X = 0; X < Block * BlockSize; X += BlockSize) { __m128i Src_Shift0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(LinePS + X)), Zero); // A7 A6 A5 A4 A3 A2 A1 A0 __m128i Src_Shift1 = _mm_slli_si128(Src_Shift0, 2); // A6 A5 A4 A3 A2 A1 A0 0 __m128i Src_Shift2 = _mm_slli_si128(Src_Shift1, 2); // 移位改爲基於Shift0,速度慢,Why? // A5 A4 A3 A2 A1 A0 0 0 __m128i Src_Shift3 = _mm_slli_si128(Src_Shift2, 2); // A4 A3 A2 A1 A0 0 0 0 __m128i Shift_Add12 = _mm_add_epi16(Src_Shift1, Src_Shift2); // A6+A5 A5+A4 A4+A3 A3+A2 A2+A1 A1+A0 A0+0 0+0 __m128i Shift_Add03 = _mm_add_epi16(Src_Shift0, Src_Shift3); // A7+A4 A6+A3 A5+A2 A4+A1 A3+A0 A2+0 A1+0 A0+0 __m128i Low = _mm_add_epi16(Shift_Add12, Shift_Add03); // A7+A6+A5+A4 A6+A5+A4+A3 A5+A4+A3+A2 A4+A3+A2+A1 A3+A2+A1+A0 A2+A1+A0+0 A1+A0+0+0 A0+0+0+0 __m128i High = _mm_add_epi32(_mm_unpackhi_epi16(Low, Zero), _mm_unpacklo_epi16(Low, Zero)); // A7+A6+A5+A4+A3+A2+A1+A0 A6+A5+A4+A3+A2+A1+A0 A5+A4+A3+A2+A1+A0 A4+A3+A2+A1+A0 __m128i SumL = _mm_loadu_si128((__m128i *)(LinePL + X + 0)); __m128i SumH = _mm_loadu_si128((__m128i *)(LinePL + X + 4)); SumL = _mm_add_epi32(SumL, PreV); SumL = _mm_add_epi32(SumL, _mm_unpacklo_epi16(Low, Zero)); SumH = _mm_add_epi32(SumH, PreV); SumH = _mm_add_epi32(SumH, High); PreV = _mm_add_epi32(PreV, _mm_shuffle_epi32(High, _MM_SHUFFLE(3, 3, 3, 3))); _mm_storeu_si128((__m128i *)(LinePD + X + 0), SumL); _mm_storeu_si128((__m128i *)(LinePD + X + 4), SumH); } for (int X = Block * BlockSize, V = LinePD[X - 1] - LinePL[X - 1]; X < Width; X++) { V += LinePS[X]; LinePD[X] = V + LinePL[X]; }
}
咱們先來解釋下這段代碼的SSE優化過程吧。spa
首先,用_mm_loadl_epi64一次性加載8個字節數據到XMM寄存器中,其中寄存器的高8位位0,此時寄存器的數據爲:code
高位 0 0 0 0 0 0 0 0 A7 A6 A5 A4 A3 A2 A1 A0 低位 (8位)blog
由於涉及到加法,而且最大爲8個字節數據的加法,所以轉換到16位數據類型,使用_mm_unpacklo_epi8結合zero便可實現。it
此時XMM寄存器內容變爲:
Src_Shift0 A7 A6 A5 A4 A3 A2 A1 A0 (16位)
此後有3次移位分別獲得:
Src_Shift1 A6 A5 A4 A3 A2 A1 A0 0 (16位) Src_Shift2 A5 A4 A3 A2 A1 A0 0 0 (16位) Src_Shift3 A4 A3 A2 A1 A0 0 0 0 (16位)
經過_mm_add_epi16分別對4組16位數據進行8次相加:
Shift_Add12 A6+A5 A5+A4 A4+A3 A3+A2 A2+A1 A1+A0 A0+0 0+0 (16位) Shift_Add03 A7+A4 A6+A3 A5+A2 A4+A1 A3+A0 A2+0 A1+0 A0+0 (16位)
再對他們進行相加:
Low A7+A6+A5+A4 A6+A5+A4+A3 A5+A4+A3+A2 A4+A3+A2+A1 A3+A2+A1+A0 A2+A1+A0+0 A1+A0+0+0 A0+0+0+0
注意到低4位的16位數已是連續相加的數據了,只要將他們轉換爲32位就能夠直接使用。
而經過 __m128i High = _mm_add_epi32(_mm_unpackhi_epi16(Low, Zero), _mm_unpacklo_epi16(Low, Zero)); 這一句則能夠把前面的高4位連續相加的值拼接起來獲得:
High A7+A6+A5+A4+A3+A2+A1+A0 A6+A5+A4+A3+A2+A1+A0 A5+A4+A3+A2+A1+A0 A4+A3+A2+A1+A0
後面的操做則瓜熟蒂落了。
注意到我核心的改動在於原始代碼中的el8shr12和el8shr03的計算中的_mm_unpacklo_epi8被消除了,而在el8shr0一句中增長了一個_mm_unpacklo_epi8,所以少了3次這個函數,很明顯這樣作是不會改變計算結果的。
另外源代碼中的部分_mm_add_epi16被我用_mm_add_epi32代替了,這主要是由於用_mm_add_epi32意義更明顯,並且因爲高位數據爲0,他們的執行結果不會有任何區別。
還有一點在測試時發現,若是Src_Shift2,Src_Shift3的移位是基於Src_Shift0,即便用以下代碼:
__m128i Src_Shift2 = _mm_slli_si128(Src_Shift0, 4); __m128i Src_Shift3 = _mm_slli_si128(Src_Shift0, 6);
速度會有較爲明顯的降低,難道說移動的位數多少和CPU的耗時有關?
以上是灰度模式的算法,在個人筆記本電腦上,SSE優化後的語句雖然增長了不少,可是執行效率約能提高30%,不過在一些PC上,普通的C和SSE優化後卻沒有啥速度區別了,這也不知道是爲何了。
若是是針對24位或者32位圖像,基本的優化思想是一致的,不過有更多的細節須要本身注意。
24位或者32位圖像在任何機器配置上,速度都能有30%的提高的。
仍是感受這種算法用文字很難表述清楚,用代碼再加上本身的空間組合可能更能理解吧。