您的位置 首页 制造

高斯含糊算法的全面优化进程共享(二)

在高斯模糊算法的全面优化过程分享(一)一文中我们已经给出了一种相当高性能的高斯模糊过程,但是优化没有终点,经过上一个星期的发愤图强和测试,对该算法的效率提升又有了一个新的高度,这里把优化过程中的一

  在高斯含糊算法的全面优化进程共享(一)一文中咱们现已给出了一种适当高功能的高斯含糊进程,可是优化没有结尾,经过上一个星期的卧薪尝胆和测验,对该算法的功率前进又有了一个新的高度,这儿把优化进程中的一些心得和收成用文字的方法记录下来。

  榜首个测验 直接运用内联汇编替代intrinsics代码(无效)

  我在某篇博客里看到说intrinsics语法尽管简化了SSE编程的难度,可是他无法直接操控XMM0-XMM7寄存器,许多指令中心都会用内存做中转,所以我就想我假如直接用汇编写功率必定还能有进一步的前进,所以我首要测验把GaussBlurFromLeftToRight_SSE优化,细心观察这个函数,假如弄得好,的确能有用的运用这些寄存器,有关代码如下:

 

  void GaussBlurFromLeftToRight_SSE(float *Data, int Width, int Height, float B0, float B1, float B2, float B3)

  {

  float *MemB3 = (float *)_mm_malloc(4 * sizeof(float), 16);

  MemB3[0] = MemB3[1] = MemB3[2] = MemB3[3] = B3;

  int Stride = Width * 4 * sizeof(float);

  _asm

  {

  mov ecx, Height

  movss xmm0, B0

  shufps xmm0, xmm0, 0 // xmm0 = B0

  movss xmm1, B1

  shufps xmm1, xmm1, 0 // xmm1 = B1

  movss xmm2, B2

  shufps xmm2, xmm2, 0 // xmm2 = B2

  mov edi, MemB3

  LoopH24 :

  mov esi, ecx

  imul esi, Stride

  add esi, Data // LinePD = Data + Y * Width * 4

  mov eax, Width

  movaps xmm3, [esi] // xmm3 = V1

  movaps xmm4, xmm3 // xmm4 = V2 = V1

  movaps xmm5, xmm3 // xmm5 = V3 = V1

  LoopW24 :

  movaps xmm6, [esi] // xmm6 = V0

  movaps xmm7, xmm3 // xmm7 = V1

  mulps xmm5, [edi] // xmm5 = V3 * B3

  mulps xmm7, xmm1 // xmm7 = V1 * B1

  mulps xmm6, xmm0 // xmm6 = V0 * B0

  addps xmm6, xmm7 // xmm6 = V0 * B0 + V1 * B1

  movaps xmm7, xmm4 // xmm7 = V2

  mulps xmm7, xmm2 // xmm7 = V2 * B2

  addps xmm5, xmm7 // xmm5 = V3 * B3 + V2 * B2

  addps xmm6, xmm5 // xmm6 = V0 * B0 + V1 * B1 + V3 * B3 + V2 * B2

  movaps xmm5, xmm4 // V3 = V2

  movaps xmm4, xmm3 // V2 = V1

  movaps [esi], xmm6

  movaps xmm3, xmm6 // V1 = V0

  add esi, 16

  dec eax

  jnz LoopW24

  dec ecx

  jnz LoopH24

  }

  _mm_free(MemB3);

  }

    

 

  看上面的代码,根本上把XMM0-XMM7这8个寄存器都充沛运用了,在我的料想中应该能有速度的前进的,可是一履行,真的好悲惨剧,和原先比较速度毫无改变,这是怎么回事呢。

  后来我反编译intrinsics的相关代码,发现编译器真的很厉害,他的汇编代码和我上面的根本共同,仅仅寄存器的运用次序有所不同罢了,后边又看了其他的几个函数,发现编译器的汇编码都写的十分高效,根本上咱们是超不过他了,并且编译器还能充沛调整指令履行的次序,使得有关指令还能完结指令层次的并行,而假如咱们自己写ASM,这个对功力的要求就更高了,所以说网络上的说法也不能够彻底信任,而假如不是有特别强的汇编才能,也不要去应战编译器。

  第二个测验 水平方向的含糊一次履行二行(15%提速)

  这个测验纯粹是随意而为,谁知道竟然十分有用果,详细而言便是在GaussBlurFromLeftToRight_SSE和GaussBlurFromRightToLeft_SSE函数的Y循环内部,一次性处理二行代码,咱们以LeftToRight为例,暗示代码如下:

    

 

  __m128 CofB0 = _mm_set_ps(0, B0, B0, B0);

  __m128 CofB1 = _mm_set_ps(0, B1, B1, B1);

  __m128 CofB2 = _mm_set_ps(0, B2, B2, B2);

  __m128 CofB3 = _mm_set_ps(0, B3, B3, B3);

  __m128 V1 = _mm_load_ps(LineP1); // 起点重复数据

  __m128 W1 = _mm_load_ps(LineP2);

  __m128 V2 = V1, V3 = V1;

  __m128 W2 = W1, W3 = W1;

  for (int X = 0; X < Length; X++, LineP1 += 4, LineP2 += 4)

  {

  __m128 V0 = _mm_load_ps(LineP1);

  __m128 W0 = _mm_load_ps(LineP2);

  __m128 V01 = _mm_add_ps(_mm_mul_ps(CofB0, V0), _mm_mul_ps(CofB1, V1));

  __m128 W01 = _mm_add_ps(_mm_mul_ps(CofB0, W0), _mm_mul_ps(CofB1, W1));

  __m128 V23 = _mm_add_ps(_mm_mul_ps(CofB2, V2), _mm_mul_ps(CofB3, V3));

  __m128 W23 = _mm_add_ps(_mm_mul_ps(CofB2, W2), _mm_mul_ps(CofB3, W3));

  __m128 V = _mm_add_ps(V01, V23);

  __m128 W = _mm_add_ps(W01, W23);

  V3 = V2; V2 = V1; V1 = V;

  W3 = W2; W2 = W1; W1 = W;

  _mm_store_ps(LineP1, V);

  _mm_store_ps(LineP2, W);

  }

    

 

  便是把本来的代码仿制一份,在略微调整一下,当然留意这个时分Y重量一非必须递加2行了,还有假如Height是奇数,还要对终究一行做处理。这些活都是细活,略微留意就不会出错了。

  就这么样的简略的一个调整,经过测验功能竟然能有15%的前进,真是意想不到,剖析详细的原因,我觉得Y循环变量的计数耗时的削减在这儿是无关紧要的,中心或许仍是这些intrinsics内部寄存器的一些调整,是的更多的指令能并行履行。

  可是,在笔直方向的SSE代码用相似的方法调整好像没有功能的前进,还会究竟代码的可读性较差。

  第三种测验:不运用中心内存完结的近似作用(80%提速)

  曾经我在写高斯含糊时考虑到内存占用问题,采用了一种近似的方法,在水平方向核算时,只需求分配一行巨细的浮点数据,然后每一行都运用这一行数据做缓存,当一行数据的水平含糊核算完后,就把这些数据转化为字节数据保存到成果图画中,当水平方向都核算完结后,在进队伍方向的处理。列方向也是只分配高度巨细的一列中心浮点缓存数据,然后进行高度方向处理,每列处理完后,把浮点的成果转化成字节数据。

  可见,上述进程存在的必定的精度丢掉,因为内行方向的处理完结后的浮点到字节数据的转化丢掉了部分数据。可是考虑到是含糊,这种丢掉关于成果在视觉上是根本发觉不到的。因而,是能够承受的,测验标明,纯C版别的这种做法和纯C版别的规范做法在速度上根本适当。

  咱们考虑这种做法的SSE优化,榜首,是水平方向的处理,想想很简略,中心的代码和之前的是没有差异的,当然咱们也应该带上咱们的两行一次性处理这种窍门的。

  可是笔直方向呢,假如依照上述方法处理,就无法运用到SSE的优势了,因为上述方法要求每次都是隔行取样,Cache miss的或许性太高,那么还能不能运用咱们在高斯含糊算法的全面优化进程共享(一)前进的那种方法呢。

  细心看看(一)中的进程,很明显他一次性只会运用到4行的数据,一同,相邻两行的处理数据有3行是堆叠的,那么这就为咱们的低内存占用一同又能高效的运用SSE供给了或许性,咱们只需求分配4行的浮点缓存区,然后每次交流行行之间的指针,对笔直方向的处理就能运用相同的SIMD优化算法了。

  可是这样做又会带来别的一个小问题,便是在Top到Bottom的处理进程中,每一行处理完后又会有一个浮点到字节数据的精度丢掉,这种丢掉经过测验也是能够承受的。

  还有一个问题便是,这样做会添加许屡次自己数据到浮点数据的转化,这种转化的耗时是否对终究的成果又重要的影响呢,只需实测才知道。咱们待会再剖析,这儿贴出这种近似的优化的有关代码:

 

  void GaussBlur_FastAndLowMemory_SSE(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, float Radius)

  {

  float B0, B1, B2, B3;

  float *Line0, *Line1, *Line2, *Line3, *Temp;

  int Y = 0;

  CalcGaussCof(Radius, B0, B1, B2, B3);

  float *Buffer = (float *)_mm_malloc(Width * 4 * 4 * sizeof(float), 16); // 最多需求4行缓冲区

  // 行方向的优化,这个是没有啥精度丢掉的

  for (; Y < Height – 1; Y += 2) // 两行履行的代码比单行快

  {

  ConvertBGR8U2BGRAF_Line_SSE(Src + (Y + 0) * Stride, Buffer, Width);

  ConvertBGR8U2BGRAF_Line_SSE(Src + (Y + 1) * Stride, Buffer + Width * 4, Width); // 读取两行数据

  GaussBlurLeftToRight_TwoLine_SSE(Buffer, Width, B0, B1, B2, B3); // 分开来履行速度比写在一同有要快些

  GaussBlurRightToLeft_TwoLine_SSE(Buffer, Width, B0, B1, B2, B3);

  ConvertBGRAF2BGR8U_Line_SSE(Buffer, Dest + (Y + 0) * Stride, Width); // 浮点转化为字节数据

  ConvertBGRAF2BGR8U_Line_SSE(Buffer, Dest + (Y + 1) * Stride, Width);

  }

  for (; Y < Height; Y++) // 履行剩余的单行

  {

  ConvertBGR8U2BGRAF_Line_SSE(Src + Y * Stride, Buffer, Width);

  GaussBlurLeftToRight_OneLine_SSE(Buffer, Width, B0, B1, B2, B3);

  GaussBlurRightToLeft_OneLine_SSE(Buffer, Width, B0, B1, B2, B3);

  ConvertBGRAF2BGR8U_Line_SSE(Buffer, Dest + Y * Stride, Width);

  }

  // 列方向考虑优化,多了一次浮点到字节类型的转化,有精度丢掉

  ConvertBGR8U2BGRAF_Line_SSE(Dest, Buffer + 3 * Width * 4, Width);

  memcpy(Buffer + 0 * Width * 4, Buffer + 3 * Width * 4, Width * 4 * sizeof(float)); // 起始值取鸿沟的值

  memcpy(Buffer + 1 * Width * 4, Buffer + 3 * Width * 4, Width * 4 * sizeof(float));

  memcpy(Buffer + 2 * Width * 4, Buffer + 3 * Width * 4, Width * 4 * sizeof(float));

  Line0 = Buffer + 0 * Width * 4; Line1 = Buffer + 1 * Width * 4;

  Line2 = Buffer + 2 * Width * 4; Line3 = Buffer + 3 * Width * 4;

  for (Y = 0; Y < Height; Y++)

  {

  ConvertBGR8U2BGRAF_Line_SSE(Dest + Y * Stride, Line3, Width); // 转化当前行到浮点缓存

  GaussBlurTopToBottom_LowMemory_SSE(Line0, Line1, Line2, Line3, Width, B0, B1, B2, B3); // 笔直方向处理

  ConvertBGRAF2BGR8U_Line_SSE(Line3, Dest + Y * Stride, Width); // 又再次转化为字节数据

  Temp = Line0; Line0 = Line1; Line1 = Line2; Line2 = Line3; Line3 = Temp; // 交流行缓存

  }

  ConvertBGR8U2BGRAF_Line_SSE(Dest + (Height – 1) * Stride, Buffer + 3 * Width * 4, Width); // 重复边际像素

  memcpy(Buffer + 0 * Width * 4, Buffer + 3 * Width * 4, Width * 4 * sizeof(float));

  memcpy(Buffer + 1 * Width * 4, Buffer + 3 * Width * 4, Width * 4 * sizeof(float));

  memcpy(Buffer + 2 * Width * 4, Buffer + 3 * Width * 4, Width * 4 * sizeof(float));

  Line0 = Buffer + 0 * Width * 4; Line1 = Buffer + 1 * Width * 4;

  Line2 = Buffer + 2 * Width * 4; Line3 = Buffer + 3 * Width * 4;

  for (Y = Height – 1; Y > 0; Y–) // 笔直向上处理

  {

  ConvertBGR8U2BGRAF_Line_SSE(Dest + Y * Stride, Line0, Width);

  GaussBlurBottomToTop_LowMemory_SSE(Line0, Line1, Line2, Line3, Width, B0, B1, B2, B3);

  ConvertBGRAF2BGR8U_Line_SSE(Line0, Dest + Y * Stride, Width);

  Temp = Line3; Line3 = Line2; Line2 = Line1; Line1 = Line0; Line0 = Temp;

  }

  _mm_free(Buffer);

  }

    

 

  上述代码中的ConvertBGR8U2BGRAF_Line_SSE和ConvertBGRAF2BGR8U_Line_SSE是之前的相关函数的单行版。

  经过测验,上述改善后的算法在相同装备的电脑上,针对3000*2000的五颜六色图画耗时约为86ms,和之前的145ms比较,提速了近一倍,而根本不占用额定的内存,可是为什么呢,好像代码中还添加了许多浮点到字节和字节到浮点数据的转化代码,总的核算量应该是添加的啊。依照我的剖析,我以为这是这儿分配的辅佐内存很小,被分配到一级缓存或许二级缓存或其他更接近CPU的方位的内尺度区域的或许性更大,而榜首版别的内存因为过大,只或许分配仓库中,一同咱们算法里有着许多拜访内存的当地,这样尽管总的转化量添加了,可是内存拜访节约的时刻现已逾越了转化添加的时刻了。

  第四种测验:列方向直接运用BGR而不是BGRA的SSE优化(100%提速)

  在高斯含糊算法的全面优化进程共享(一)中,为了处理水平方向上的SSE优化问题,咱们将BGR数据转化为了BGRA格局的浮点数后再进行处理,这样在列方向处理时相同需求处理A的数据,可是在经过第三种测验后,在笔直方向的处理咱们还有必要处理这个剩余的A吗,当然没有必要,这样笔直方向全体上又能够削减约25%的时刻,耗时只需75ms左右了,完结了约100%的提速。

  第五种测验:算法稳定性的考虑和终究的退让

  在上一篇文章中,咱们提到了因为float类型的精度问题,当含糊的半径较大时,算法的成果会呈现很大的瑕疵,一种方法便是用double类型来处理,还有一种方法便是能够用Deriche滤波器来处理,为了完美处理这个问题,我仍是恨着头皮用SSE完结了Deriche滤波器,这儿扼要阐明如下:

  Deriche滤波器和高斯滤波器有许多相似的当地:The Deriche filter is a smoothing filter (low-pass) which was designed to optimally detect, along with a derivation operator, the contours in an image (Canny criteria optimization). Besides, as this filter is very similar to a gaussian filter, but much simpler to implement (based on simple first order IIR filters), it is also much used for general image filtering.

  依照维基的解说,Deriche滤波器可依照如下的进程履行:详见https://en.wikipedia.org/wiki/Deriche_edge_detector。

  It's possible to separate the process of obtaining the value of a two-dimensional Deriche filter into two parts. In first part, image array is passed in the horizontal direction from left to right according to the following formula:

  and from right to left according to the formula:

  The result of the computation is then stored into temporary two-dimensional array:

  The second step of the algorithm is very similar to the first one. The two-dimensional array from the previous step is used as the input. It is then passed in the vertical direction from top to bottom and bottom-up according to the following formulas:

  可见他们也是队伍可分离的算法。

  相同为了节约内存,咱们也运用了相似上述第三种和第四重测验的方法,可是考虑到Deriche的特殊性(主要是这儿),他仍是需求一份中心内存的,为了功率和内存,咱们再次以献身精度为预备,中心运用了一份和图画相同的字节数据内存。

  因为核算量较原先的高斯有所添加,这儿终究的优化完结的耗时约为100ms。

  第六:多线程

  在水平方向核算时,各行之间的核算时独立的,因而是能够并行处理的,可是笔直方向由所曾经后依靠的,是无法并行的。比方用openmp做2个线程的并行,大约速度能前进到(高斯)到60ms,可是这个东西在不是本文这儿的要点。

  第七:比较

  同规范的高斯滤波比较,Deriche滤波器因为其特性,无法支撑In-Place操作,也便是说Src和Dest不能相同,假如必定要相同,就只需经过一个中心内存来过渡了,而规范高斯是能够的。第二便是高斯是能够不占用太多额定的内存就能够完结的,而Deriche需求一份相同巨细的内存。第三便是规范高斯速度仍是要快一点。第四Deriche滤波器的精度在float类型时精度要比规范高斯高。归纳挑选,我觉得仍是今后挑选Deriche替代规范的高斯含糊。

  总结:有心就有成果

  同opencv的cvsmooth比较,相同的机器上相同的3000*2000巨细的彩图,Ksize我取100时,需求1200多ms,比我这儿慢了10倍,可是cvsmooth好像对ksize参数灵敏,他并不是与核巨细无关的,ksize较小时还会很快的,不过除了一些特效外,在许多场合咱们其实需求大半径的高斯的(比方图画增强、锐化上)。

  做完了在回头看看优化的进程,觉得和看书是一个道理,先是越看越厚,通了就像一张薄纸相同。

  终究总结下,便是一件工作,只需你有时刻和决心,就能有前进,坚持是成功的必要条件。

  供给一个测验的Demo: http://share.eepw.com.cn/share/download/id/383731

  由测验Demo能够测验出,当挑选低内存近似版别或许精确版别时,当半径较大时,假如接连的拖动滚动条,图画会呈现闪耀,而假如挑选Deriche时,则图画改换很陡峭,而当半径特别大时,假如挑选低内存近似版别或许精确版别,则图画有或许会呈现线条或许色块,只需Deriche滤波的作用是完美的。

 

  高斯含糊的优化到此结束,假如有谁有用GPU完结的,还请告诉我下大约的耗时。

  回绝无脑讨取代码。

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/bandaoti/zhizao/114907.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部