SSE图像算法优化体系二,高斯模糊算法的完美优化进度分享

     
相关链接: 高斯模糊算法的应有尽有优化进程分享(一)

     
相关链接: 高斯模糊算法的周到优化进程分享(一)

   
 在高斯模糊算法的两全优化过程分享(一)一文中我们早已交给了一种非常高品质的高斯模糊进程,然则优化没有终点,经过上三个星期的斗争和测试,对该算法的作用升高又有了1个新的中度,那里把优化进度中的一些感受和得到用文字的花样记录下来。

   
 在高斯模糊算法的周密优化进度分享(一)一文中大家曾经交由了一种杰出高质量的高斯模糊进程,可是优化没有极限,经过上2个礼拜的加油和测试,对该算法的频率进步又有了1个新的可观,那里把优化进度中的一些体验和获得用文字的花样记录下来。

     第3个尝试   直接选用内联系汇率编替代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那九个寄存器都充裕利用了,在本身的预想中应该能有速度的晋升的,然则一执行,真的好正剧,和原来比较速度并非变化,这是怎么回事呢。

     
后来小编反编写翻译intrinsics的相关代码,发现编写翻译器真的相当厉害,他的汇编代码和自个儿下面的基本一致,只是寄存器的利用顺序有所不相同而已,后边又看了别样的多少个函数,发现编写翻译器的汇编码都写的可怜高效,基本上大家是超不过她了,而且编写翻译器还是能丰富调动指令执行的各种,使得有关指令仍是能够落到实处指令层次的相互,而一旦我们友好写ASM,那个对武术的须求就更高了,所以说网络上的说法也不得以完全相信,而一旦不是有尤其强的汇编能力,也决不去挑战编写翻译器。

     第2个尝试   直接使用内联汇编替代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那七个寄存器都足够利用了,在本身的预想中应该能有速度的升级换代的,不过一执行,真的好喜剧,和原先比较速度并非变化,那是怎么回事呢。

     
后来自家反编写翻译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代码用类似的章程调动就如没有品质的升级换代,还会到底代码的可读性较差。

  第两种尝试:不使用在那之中内部存款和储蓄器达成的好像效果(4/5提速)

   
 从前小编在写高斯模糊时考虑到内存占用难题,选用了一种恍若的章程,在档次方向计算时,只必要分配一行大小的浮点数据,然后每一行都应用这一行数据做缓存,当一行数据的档次模糊计算完后,就把这么些数量转换为字节数据保存到结果图像中,当水平方向都盘算达成后,在进展列方向的处理。列方向也是只分红中度大小的一列中间浮点缓存数据,然后开展中度方向处理,每列处理完后,把浮点的结果转换成字节数据。

   
 可知,上述进度存在的自可是然的精度损失,因为在行方向的处理到位后的浮点到字节数据的转移丢失了一些数量。不过考虑到是混淆,那种丢失对于结果在视觉上是主导察觉不到的。因而,是还行的,测试申明,纯C版本的那种做法和纯C版本的科班做法在进度上基本优异。

   
 大家考虑那种做法的SSE优化,第叁,是水平方向的拍卖,想想很简短,大旨的代码和前边的是从未分其他,当然我们也应当带上大家的两行一次性处理那种诀窍的。

   
 然而垂直方向呢,假使依照上述措施处理,就十分小概采纳到SSE的优势了,因为上述方法需要每一次都以隔行取样,Cache
miss的大概性太高,那么还是可以够不能够动用大家在高斯模糊算法的宏观优化进度分享(一)抓好的那种方式啊。

   
 仔细看看(一)中的进程,很明朗他三次性只会选拔到4行的多少,同时,相邻两行的拍卖多少有3行是重叠的,那么那就为我们的低内部存款和储蓄器占用同时又能飞快的施用SSE提供了大概,我们只须求分配4行的浮点缓存区,然后每一回沟通行行之间的指针,对垂直方向的处理就能利用同一的SIMD优化算法了。

   
 但是那样做又会带来别的3个小标题,便是在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);
 }

  上述代码中的ConvertBGLacrosse8U2BGRAF_Line_SSE和ConvertBGRAF2BGR8U_Line_SSE是事先的连带函数的单行版。

     
经过测试,上述创新后的算法在同样配备的微型总括机上,针对2000*两千的彩色图像耗费时间约为86ms,和事先的145ms相比,提速了近一倍,而基本不占用额外的内部存款和储蓄器,不过为何吗,如同代码中还扩充了过多浮点到字节和字节到浮点数据的转移代码,总的计算量应该是充实的呦。依照自个儿的辨析,小编以为那是此处分配的增派内部存款和储蓄器相当小,被分配到一流缓存大概二级缓存或任何更近乎CPU的职责的内尺寸区域的大概性更大,而首先版本的内部存储器由于过大,只或然分配堆栈中,同时我们算法里装有大量访问内部存款和储蓄器的地点,那样尽管总的转换量扩充了,不过内存访问节省的光阴已经超(英文名:jīng chāo)越了更换扩充的时光了。

  第各样尝试:列方向一贯动用BG揽胜而不是BGRA的SSE优化(100%提速)

     
高斯模糊算法的两全优化进度分享(一)中,为了缓解程度方向上的SSE优化难题,我们将BGQashqai数据转换为了BGRA格式的浮点数后再展开始拍戏卖,那样在列方向处理时一致供给处理A的数量,不过在通过第二种尝试后,在笔直方向的拍卖大家还有必不可少处理那么些多余的A吗,当然没有供给,那样垂直方向全部上又能够减少约肆分之一的年华,耗费时间唯有75ms左右了,完结了约百分百的涨潮。

    第3个尝试   水平方向的歪曲1遍执行二行(15%提速)

   
 那些尝试纯粹是专擅而为,哪个人知道如故11分有效果,具体而言就是在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代码用接近的主意调动如同没有品质的升高,还会到底代码的可读性较差。

  第二种尝试:不行使个中内部存款和储蓄器达成的好像效果(4/5提速)

   
 从前自个儿在写高斯模糊时考虑到内部存款和储蓄器占用难题,接纳了一种类似的措施,在档次方向总计时,只必要分配一行大小的浮点数据,然后每一行都使用这一行数据做缓存,当一行数据的水平模糊总计完后,就把这一个数量转换为字节数据保存到结果图像中,当水平方向都划算完毕后,在拓展列方向的处理。列方向也是只分红高度大小的一列中间浮点缓存数据,然后开始展览高度方向处理,每列处理完后,把浮点的结果转换到字节数据。

   
 可见,上述进程存在的一定的精度损失,因为在行方向的拍卖到位后的浮点到字节数据的转移丢失了有些多少。可是考虑到是模糊,那种丢失对于结果在视觉上是主导察觉不到的。因而,是还行的,测试表明,纯C版本的那种做法和纯C版本的正规做法在进程上着力极度。

   
 我们考虑这种做法的SSE优化,第壹,是程度方向的处理,想想很简短,宗旨的代码和前边的是从未区分的,当然大家也应当带上大家的两行二遍性处理那种诀窍的。

   
 不过笔直方向呢,倘若依据上述形式处理,就不能够使用到SSE的优势了,因为上述办法须要每一次都以隔行取样,Cache
miss的大概太高,那么还是能够不可能使用大家在高斯模糊算法的八面玲珑优化进度分享(一)升高的那种办法啊。

   
 仔细看看(一)中的进程,很备受瞩目他三遍性只会动用到4行的数量,同时,相邻两行的拍卖数量有3行是重叠的,那么那就为我们的低内存占用同时又能便捷的运用SSE提供了大概性,大家只必要分配4行的浮点缓存区,然后每一回调换行行之间的指针,对垂直方向的拍卖就能利用同一的SIMD优化算法了。

   
 可是如此做又会推动其它3个不奇怪,正是在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);
 }

  上述代码中的ConvertBG福睿斯8U2BGRAF_Line_SSE和ConvertBGRAF2BGR8U_Line_SSE是事先的连锁函数的单行版。

     
经过测试,上述革新后的算法在平等配备的电脑上,针对三千*两千的彩色图像耗费时间约为86ms,和以前的145ms比较,提速了近一倍,而基本不占用额外的内存,不过为何吧,如同代码中还扩展了成都百货上千浮点到字节和字节到浮点数据的转移代码,总的总计量应该是充实的呀。根据小编的辨析,笔者觉得那是这里分配的救助内部存款和储蓄器十分的小,被分配到一流缓存只怕二级缓存或其他更接近CPU的地点的内尺寸区域的大概性更大,而首先本子的内部存款和储蓄器由于过大,只或然分配堆栈中,同时大家算法里具有大批量造访内部存款和储蓄器的地点,那样即使总的转换量扩展了,不过内部存储器访问节省的时间已经超先生过了更换扩充的时刻了。

  第三种尝试:列方向平素利用BGRubicon而不是BGRA的SSE优化(百分百提速)

     
高斯模糊算法的公而忘私优化进度分享(一)中,为了消除程度方向上的SSE优化难点,大家将BG宝马7周到据转换为了BGRA格式的浮点数后再展开始拍录卖,这样在列方向处理时一样需求处理A的数码,不过在通过第二种尝试后,在笔直方向的处理大家还有供给处理那个多余的A吗,当然没有要求,那样垂直方向全部上又能够削减约四分之一的时光,耗费时间唯有75ms左右了,实现了约百分之百的涨价。

      第④种尝试:算法稳定性的设想和结尾的投降

  在上一篇小说中,大家提到了由于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 IIEscort 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:

图片 1

and from right to left according to the formula:

图片 2

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

图片 3 
                                                

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:

图片 4

图片 5

图片 6

  可知他们也是行列可分其他算法。

   
 同样为了节省外部存储器,大家也应用了近乎上述第三种和第④重尝试的方式,可是考虑到Deriche的特殊性(首假设图片 7此处),他要么须求一份中间内部存储器的,为了效用和内部存款和储蓄器,大家重新以牺牲精度为准备,中间使用了一份和图像一样的字节数据内部存款和储蓄器。

   
由于总括量较原先的高斯有所增多,那里最终的优化完毕的耗费时间约为100ms。

      第肆种尝试:算法稳定性的设想和结尾的和解

  在上一篇小说中,大家关系了由于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 IILX570 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:

图片 8

and from right to left according to the formula:

图片 9

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

图片 10 
                                                

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:

图片 11

图片 12

图片 13

  可知他们也是行列可分其他算法。

   
 同样为了节省里部存款和储蓄器,大家也使用了近乎上述第二种和第⑤重尝试的办法,不过考虑到Deriche的特殊性(首假使图片 14那边),他依旧需求一份中间内部存款和储蓄器的,为了功能和内部存款和储蓄器,咱们再一次以献身精度为准备,中间使用了一份和图像一样的字节数据内部存款和储蓄器。

   
由于总计量较原先的高斯有所增多,那里最终的优化达成的耗时约为100ms。

     第六:多线程

   
 在档次方向总括时,各行之间的持筹握算时单身的,由此是能够并行处理的,但是垂直方向由于是上下信赖的,是无能为力并行的。比如用openmp做三个线程的互相,差不离速度能增加到(高斯)到60ms,可是这几个事物在不是本文这里的严重性。

     第六:多线程

   
 在档次方向总括时,各行之间的计量时独立的,由此是可以并行处理的,但是垂直方向由于是上下倚重的,是无力回天并行的。比如用openmp做3个线程的并行,大约速度能拉长到(高斯)到60ms,可是这么些事物在不是本文那里的根本。

  第七:比较

   
同专业的高斯滤波相比较,Deriche滤波器由于其特点,不大概支撑In-Place操作,也正是说Src和Dest不能够平等,假诺一定要一如既往,就唯有由此1个个中内部存款和储蓄器来过渡了,而正规高斯是能够的。第3正是高斯是足以不占用太多额外的内部存款和储蓄器就能够达成的,而Deriche须求一份同样大小的内部存款和储蓄器。第②正是专业高斯速度依然要快一些。第肆Deriche滤波器的精度在float类型时精度要比标准高斯高。综合取舍,笔者以为依然今后选拔Deriche代替标准的高斯模糊。

  第七:比较

   
同标准的高斯滤波相比较,Deriche滤波器由于其特征,不或然支撑In-Place操作,也正是说Src和Dest无法一如既往,假若一定要平等,就唯有经过3个中档内部存款和储蓄器来过渡了,而正式高斯是能够的。第叁就是高斯是能够不占用太多额外的内部存款和储蓄器就足以兑现的,而Deriche要求一份同样大小的内部存款和储蓄器。第2就是行业内部高斯速度仍旧要快一些。第4Deriche滤波器的精度在float类型时精度要比正规高斯高。综合取舍,笔者以为如故之后选取Deriche代替标准的高斯模糊。

     计算:有心就有战绩

   
同opencv的cvsmooth相比较,同样的机器上平等的三千*三千轻重的彩图,Ksize笔者取100时,必要1200多ms,比本人那里慢了10倍,不过cvsmooth就如对ksize参数敏感,他并不是与核大小毫不相关的,ksize较小时还会火速的,可是除了有些神效外,在广大地方大家其实须要大半径的高斯的(比如图像增强、锐化上)。

   
做完了在回头看看优化的长河,觉得和看书是二个道理,先是越看越厚,通了就像一张薄纸一样。

   
最终总括下,正是一件业务,只要你有时间和自信心,就能有提升,百折不挠是小胜的须要条件。

   
提供一个测试的德姆o: http://files.cnblogs.com/files/Imageshop/FastGaussBlur.rar

   
由测试Demo能够测试出,当选用低内部存款和储蓄器近似版本或然纯粹版本时,当半径较大时,要是总是的拖动滚动条,图像会并发闪烁,而一旦选取Deriche时,则图像变换很温情,而当半径越发大时,假若选用低内部存款和储蓄器近似版本可能纯粹版本,则图像有恐怕会油然则生线条依旧色块,唯有Deriche滤波的作用是宏观的。

图片 15

  高斯模糊的优化到此甘休,假设有哪个人有用GPU达成的,还请告知自身下大致的耗费时间。

     拒绝无脑索取代码。

图片 16

 

     总括:有心就有成就

   
同opencv的cvsmooth相比较,同样的机械上亦然的两千*三千高低的彩图,Ksize笔者取100时,须求1200多ms,比作者那边慢了10倍,可是cvsmooth就好像对ksize参数敏感,他并不是与核大小毫无干系的,ksize较时辰还会快速的,不过除了有的特效外,在不胜枚举场所我们实在必要大半径的高斯的(比如图像增强、锐化上)。

   
做完了在自查自纠看看优化的历程,觉得和看书是八个道理,先是越看越厚,通了就如一张薄纸一样。

   
最终计算下,正是一件工作,只要您有时光和信心,就能有提升,坚定不移是胜利的供给条件。

   
提供一个测试的德姆o: http://files.cnblogs.com/files/Imageshop/FastGaussBlur.rar

   
由测试德姆o能够测试出,当选用低内部存款和储蓄器近似版本只怕纯粹版本时,当半径较大时,借使延续的拖动滚动条,图像会产出闪烁,而一旦选取Deriche时,则图像变换很温柔,而当半径尤其大时,假若选择低内部存款和储蓄器近似版本恐怕纯粹版本,则图像有或许会现出线条照旧色块,只有Deriche滤波的成效是圆满的。

图片 17

  高斯模糊的优化到此结束,假如有什么人有用GPU达成的,还请告知作者下大概的耗费时间。

     拒绝无脑索取代码。

图片 18