這半年多時間,基本都在折騰一些基本的優化,有不少都是十幾年前的技術了,從隨大流的角度來考慮,研究這些東西在不少人看來是浪費時間了,即不能賺錢,也對工做能力提高無啥幫助。可我以爲人類所謂的幸福,能夠分爲物質檔次的享受,還有更爲複雜的精神上的富有,哪怕這種富有隻是存在於短暫的自我知足中也是值得的。 算法
閒話少說, SIMD指令集,這個古老的東西,從第一代開始算起,也快有近20年的歷史了,從最開始的MMX技術,到SSE,以及後來的SSE二、SSE三、SSE四、AVX以及11年之後的AVX2,逐漸的成熟和豐富,不過目前考慮通用性方面,AVX的輻射範圍仍是有限,大部分在優化時仍是考慮使用128位的SSE指令集,我在以前的一系列文章中,也有很多文章涉及到了這個方面的優化了。網絡
今天咱們來學習下Sobel算法的優化,首先,咱們給出傳統的C++實現的算法代碼:ide
int IM_Sobel(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride) { int Channel = Stride / Width; if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE; if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER; if ((Channel != 1) && (Channel != 3)) return IM_STATUS_INVALIDPARAMETER; unsigned char *RowCopy = (unsigned char *)malloc((Width + 2) * 3 * Channel); if (RowCopy == NULL) return IM_STATUS_OUTOFMEMORY; unsigned char *First = RowCopy; unsigned char *Second = RowCopy + (Width + 2) * Channel; unsigned char *Third = RowCopy + (Width + 2) * 2 * Channel; memcpy(Second, Src, Channel); memcpy(Second + Channel, Src, Width * Channel); // 拷貝數據到中間位置 memcpy(Second + (Width + 1) * Channel, Src + (Width - 1) * Channel, Channel); memcpy(First, Second, (Width + 2) * Channel); // 第一行和第二行同樣 memcpy(Third, Src + Stride, Channel); // 拷貝第二行數據 memcpy(Third + Channel, Src + Stride, Width * Channel); memcpy(Third + (Width + 1) * Channel, Src + Stride + (Width - 1) * Channel, Channel); for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePD = Dest + Y * Stride; if (Y != 0) { unsigned char *Temp = First; First = Second; Second = Third; Third = Temp; } if (Y == Height - 1) { memcpy(Third, Second, (Width + 2) * Channel); } else { memcpy(Third, Src + (Y + 1) * Stride, Channel); memcpy(Third + Channel, Src + (Y + 1) * Stride, Width * Channel); // 因爲備份了前面一行的數據,這裏即便Src和Dest相同也是沒有問題的 memcpy(Third + (Width + 1) * Channel, Src + (Y + 1) * Stride + (Width - 1) * Channel, Channel); } if (Channel == 1) { for (int X = 0; X < Width; X++) { int GX = First[X] - First[X + 2] + (Second[X] - Second[X + 2]) * 2 + Third[X] - Third[X + 2]; int GY = First[X] + First[X + 2] + (First[X + 1] - Third[X + 1]) * 2 - Third[X] - Third[X + 2]; LinePD[X] = IM_ClampToByte(sqrtf(GX * GX + GY * GY + 0.0F)); } } else { for (int X = 0; X < Width * 3; X++) { int GX = First[X] - First[X + 6] + (Second[X] - Second[X + 6]) * 2 + Third[X] - Third[X + 6]; int GY = First[X] + First[X + 6] + (First[X + 3] - Third[X + 3]) * 2 - Third[X] - Third[X + 6]; LinePD[X] = IM_ClampToByte(sqrtf(GX * GX + GY * GY + 0.0F)); } } } free(RowCopy); return IM_STATUS_OK; }
代碼很短,可是這段代碼很經典,第一,這個代碼支持In-Place操做,也就是Src和Dest能夠是同一塊內存,第二,這個代碼本質就支持邊緣。網絡上不少參考代碼都是隻處理中間有效的區域。具體的實現細節我不肯意多述,本身看。函數
那麼Sobel的核心就是計算X方向的梯度GX和Y方向的梯度GY,最後有一個耗時的操做是求GX*GX+GY*GY的平方。學習
上面這段代碼,在不打開編譯器的SSE優化和快速浮點計算的狀況,直接使用FPU,對4000*3000的彩色圖約須要480ms,當開啓SSE後,大概時間爲220ms ,所以系統編譯器的SSE優化也很厲害,反編譯後能夠看到彙編裏這樣的部分:測試
59AD12F8 movd xmm0,ecx
59AD12FC cvtdq2ps xmm0,xmm0
59AD12FF sqrtss xmm0,xmm0
59AD1303 cvttss2si ecx,xmm0
可見他是調用的單浮點數的sqrt優化。優化
因爲該Sobel的過程最後是把數據用圖像的方式記錄下來,所以,IM_ClampToByte(sqrtf(GX * GX + GY * GY + 0.0F))能夠用查表的方式來實現。簡單改爲以下的版本, 避免了浮點計算。spa
int IM_SobelTable(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride) { int Channel = Stride / Width; if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE; if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER; if ((Channel != 1) && (Channel != 3)) return IM_STATUS_INVALIDPARAMETER; unsigned char *RowCopy = (unsigned char *)malloc((Width + 2) * 3 * Channel); if (RowCopy == NULL) return IM_STATUS_OUTOFMEMORY; unsigned char *First = RowCopy; unsigned char *Second = RowCopy + (Width + 2) * Channel; unsigned char *Third = RowCopy + (Width + 2) * 2 * Channel; memcpy(Second, Src, Channel); memcpy(Second + Channel, Src, Width * Channel); // 拷貝數據到中間位置 memcpy(Second + (Width + 1) * Channel, Src + (Width - 1) * Channel, Channel); memcpy(First, Second, (Width + 2) * Channel); // 第一行和第二行同樣 memcpy(Third, Src + Stride, Channel); // 拷貝第二行數據 memcpy(Third + Channel, Src + Stride, Width * Channel); memcpy(Third + (Width + 1) * Channel, Src + Stride + (Width - 1) * Channel, Channel); int BlockSize = 16, Block = (Width * Channel) / BlockSize; unsigned char Table[65026]; for (int Y = 0; Y < 65026; Y++) Table[Y] = (sqrtf(Y + 0.0f) + 0.5f); for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePD = Dest + Y * Stride; if (Y != 0) { unsigned char *Temp = First; First = Second; Second = Third; Third = Temp; } if (Y == Height - 1) { memcpy(Third, Second, (Width + 2) * Channel); } else { memcpy(Third, Src + (Y + 1) * Stride, Channel); memcpy(Third + Channel, Src + (Y + 1) * Stride, Width * Channel); // 因爲備份了前面一行的數據,這裏即便Src和Dest相同也是沒有問題的 memcpy(Third + (Width + 1) * Channel, Src + (Y + 1) * Stride + (Width - 1) * Channel, Channel); } if (Channel == 1) { for (int X = 0; X < Width; X++) { int GX = First[X] - First[X + 2] + (Second[X] - Second[X + 2]) * 2 + Third[X] - Third[X + 2]; int GY = First[X] + First[X + 2] + (First[X + 1] - Third[X + 1]) * 2 - Third[X] - Third[X + 2]; LinePD[X] = Table[IM_Min(GX * GX + GY * GY, 65025)]; } } else { for (int X = 0; X < Width * 3; X++) { int GX = First[X] - First[X + 6] + (Second[X] - Second[X + 6]) * 2 + Third[X] - Third[X + 6]; int GY = First[X] + First[X + 6] + (First[X + 3] - Third[X + 3]) * 2 - Third[X] - Third[X + 6]; LinePD[X] = Table[IM_Min(GX * GX + GY * GY, 65025)]; } } } free(RowCopy); return IM_STATUS_OK; }
對4000*3000的彩色圖約須要180ms,比系統的SSE優化快了40ms,而這個過程徹底無浮點計算,所以,能夠知道計算GX和GY的耗時在本例中也佔據了至關大的部分。code
這樣的過程最適合於SSE處理了。blog
咱們分析之。
第一來看一看這兩句:
int GX = First[X] - First[X + 2] + (Second[X] - Second[X + 2]) * 2 + Third[X] - Third[X + 2]; int GY = First[X] + First[X + 2] + (First[X + 1] - Third[X + 1]) * 2 - Third[X] - Third[X + 2];
裏面涉及到了8個不一樣的像素,考慮計算的特性和數據的範圍,在內部計算時這個int能夠用short代替,也就是要把加載的字節圖像數據轉換爲short類型先,這樣SSE優化方式爲用8個SSE變量分別記錄8個連續的像素風量的顏色值,每一個顏色值用16位數據表達。
這可使用_mm_unpacklo_epi8配合_mm_loadl_epi64實現:
__m128i FirstP0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(First + X)), Zero); __m128i FirstP1 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(First + X + 3)), Zero); __m128i FirstP2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(First + X + 6)), Zero); __m128i SecondP0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Second + X)), Zero); __m128i SecondP2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Second + X + 6)), Zero); __m128i ThirdP0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Third + X)), Zero); __m128i ThirdP1 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Third + X + 3)), Zero); __m128i ThirdP2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Third + X + 6)), Zero);
接着就是搬積木了,用SSE的指令代替普通的C的函數指令實現GX和GY的計算。
__m128i GX16 = _mm_abs_epi16(_mm_add_epi16(_mm_add_epi16(_mm_sub_epi16(FirstP0, FirstP2), _mm_slli_epi16(_mm_sub_epi16(SecondP0, SecondP2), 1)), _mm_sub_epi16(ThirdP0, ThirdP2))); __m128i GY16 = _mm_abs_epi16(_mm_sub_epi16(_mm_add_epi16(_mm_add_epi16(FirstP0, FirstP2), _mm_slli_epi16(_mm_sub_epi16(FirstP1, ThirdP1), 1)), _mm_add_epi16(ThirdP0, ThirdP2)));
找個時候的GX16和GY16裏保存的是8個16位的中間結果,因爲SSE只提供了浮點數的sqrt操做,咱們必須將它們轉換爲浮點數,那麼這個轉換的第一步就必須是先將它們轉換爲int的整形數,這樣,就必須一個拆成2個,即:
__m128i GX32L = _mm_unpacklo_epi16(GX16, Zero); __m128i GX32H = _mm_unpackhi_epi16(GX16, Zero); __m128i GY32L = _mm_unpacklo_epi16(GY16, Zero); __m128i GY32H = _mm_unpackhi_epi16(GY16, Zero);
接着又是搬積木了:
__m128i ResultL = _mm_cvtps_epi32(_mm_sqrt_ps(_mm_cvtepi32_ps(_mm_add_epi32(_mm_mullo_epi32(GX32L, GX32L), _mm_mullo_epi32(GY32L, GY32L)))));
__m128i ResultH = _mm_cvtps_epi32(_mm_sqrt_ps(_mm_cvtepi32_ps(_mm_add_epi32(_mm_mullo_epi32(GX32H, GX32H), _mm_mullo_epi32(GY32H, GY32H)))));
整形轉換爲浮點數,最後計算完以後又要將浮點數轉換爲整形數。
最後一步,獲得8個int型的結果,這個結果有要轉換爲字節類型的,而且這些數據有可能會超出字節所能表達的範圍,因此就須要用到SSE自帶的抗飽和向下打包函數了,以下所示:
_mm_storel_epi64((__m128i *)(LinePD + X), _mm_packus_epi16(_mm_packus_epi32(ResultL, ResultH), Zero));
Ok, 一切搞定了,還有一些細節處理本身慢慢補充吧。
運行,哇,只要37ms了,速度快了N倍,可結果彷佛和其餘方式實現的不同啊,怎麼回事。
我也是找了半天都沒有找到問題所在,後來一步一步的測試,最終問題定位在16位轉換爲32位整形那裏去了。
一般,咱們都是對像素的字節數據進行向上擴展,他們都是正數,因此用unpack之類的配合zero把高8位或高16位的數據填充爲0就能夠了,可是在本例中,GX16或者GY16頗有多是負數,而負數的最高位是符號位,若是都填充爲0,則變爲正數了,明顯改變原始的數據了,因此獲得了錯誤的結果。
那如何解決問題呢,對於本例,很簡單,由於後面只有一個平方操做,所以,對GX先取絕對值是不會改變計算的結果的,這樣就不會出現負的數據了,修改以後,果真結果正確。
還能夠繼續優化,咱們來看最後的計算GX*GX+GY*GY的過程,咱們知道,SSE3提供了一個_mm_madd_epi16指令,其做用爲:
r0 := (a0 * b0) + (a1 * b1) r1 := (a2 * b2) + (a3 * b3) r2 := (a4 * b4) + (a5 * b5) r3 := (a6 * b6) + (a7 * b7)
若是咱們能把GX和GY的數據拼接成另外兩個數據:
GXYL = GX0 GY0 GX1 GY1 GX2 GY2 GX3 GY3
GXYH = GX4 GY4 GX5 GY5 GX6 GY6 GX7 GY7
那麼調用_mm_madd_epi16(GXYL ,GXYL )和_mm_madd_epi16(GXYH ,GXYH )不就是能獲得和以前同樣的結果了嗎,而這個拼接SSE已經有現成的函數了即:
__m128i GXYL = _mm_unpacklo_epi16(GX16, GY16);
__m128i GXYH = _mm_unpackhi_epi16(GX16, GY16);
這樣就把原來須要的10個指令變爲了4個指令,代碼簡潔了而且速度也更快了。
嘗試如此修改,整個的計算過程時間減小到了32ms左右。
另外,還有一個能夠優化的地方就是借用 _mm_maddubs_epi16 函數實現像素之間的加減乘除和擴展。
這個函數的做用以下:
r0 := SATURATE_16((a0 * b0) + (a1 * b1)) r1 := SATURATE_16((a2 * b2) + (a3 * b3)) ... r7 := SATURATE_16((a14 * b14) + (a15 * b15))
他的第一個參數是16個無符號的字節數據,第二個參數是16個有符號的char數據。
配合unpack使用相似上面的技術就能夠一次性處理16個字節的像素簡加減了,這樣作整個過程大概能再加速2ms,達到最終的30ms。
源代碼地址:http://files.cnblogs.com/files/Imageshop/Sobel.rar (其中的SSE代碼請按照本文的思路自行整理。)
http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,這裏是一個我所有用SSE優化的圖像處理的Demo,有興趣的朋友能夠看看。
歡迎點贊和打賞。