【问题标题】:How to optimize simple gaussian filter for performance?如何优化简单高斯滤波器的性能?
【发布时间】:2019-01-24 02:50:23
【问题描述】:

我正在尝试编写一个需要为多个全分辨率图像计算高斯和拉普拉斯金字塔的 android 应用程序,我用 NDK 在 C++ 上编写了它,代码的最关键部分是将高斯滤波器应用于图像 abd 我是水平和垂直应用此过滤器。

过滤器为 (0.0625, 0.25, 0.375, 0.25, 0.0625) 由于我正在处理整数,因此我正在计算 (1, 4, 6, 4, 1)/16

dst[index] = ( src[index-2] + src[index-1]*4 + src[index]*6+src[index+1]*4+src[index+2])/16;

我做了一些简单的优化,但它的运行速度仍然比预期的慢,我想知道是否还有其他优化选项我遗漏了。

PS:我应该提一下,我曾尝试使用内联臂组件编写此过滤器部件,但它的结果会慢 2 倍。

//horizontal  filter
for(unsigned y = 0; y < height;  y++) {
    for(unsigned x = 2; x < width-2;  x++) {
        int index = y*width+x;
            dst[index].r = (src[index-2].r+ src[index+2].r + (src[index-1].r + src[index+1].r)*4 + src[index].r*6)>>4;
            dst[index].g = (src[index-2].g+ src[index+2].g + (src[index-1].g + src[index+1].g)*4 + src[index].g*6)>>4;
            dst[index].b = (src[index-2].b+ src[index+2].b + (src[index-1].b + src[index+1].b)*4 + src[index].b*6)>>4;                
     }
}
//vertical filter
for(unsigned y = 2;  y < height-2;  y++) {
    for(unsigned x = 0;  x < width;  x++) {
        int index = y*width+x;
            dst[index].r = (src[index-2*width].r + src[index+2*width].r  + (src[index-width].r + src[index+width].r)*4 + src[index].r*6)>>4;
            dst[index].g = (src[index-2*width].g + src[index+2*width].g  + (src[index-width].g + src[index+width].g)*4 + src[index].g*6)>>4;
            dst[index].b = (src[index-2*width].b + src[index+2*width].b  + (src[index-width].b + src[index+width].b)*4 + src[index].b*6)>>4;
     }
}

【问题讨论】:

  • 你能用RGBA代替RGB吗?在这种情况下,您将有 4 个值,顺便说一下,这适合大多数平台上的 SIMD 寄存器。然后,您可以使用 SIMD 指令(例如 SSE 或 Neon)并行执行 R、G 和 B(以及您丢弃的 alpha)的操作。
  • 您可以将乘法y * width提取到外循环。与2 * width 相同:计算一次,使用多个。但也许编译器已经在做这些优化了。
  • 无论如何,将 rgb 三元组对齐到 dword 边界是值得的。

标签: c++ c performance optimization


【解决方案1】:

index 乘法可以被排除在内部循环之外,因为乘法仅在 y 更改时发生:

for (unsigned y ...
{
    int index = y * width;
    for (unsigned int x...  

您可以通过在使用变量之前加载它们来获得一些速度。这将使处理器将它们加载到缓存中:

for (unsigned x = ...  
{  
    register YOUR_DATA_TYPE a, b, c, d, e;
    a = src[index - 2].r;
    b = src[index - 1].r;
    c = src[index + 0].r; // The " + 0" is to show a pattern.
    d = src[index + 1].r;
    e = src[index + 2].r;
    dest[index].r = (a + e + (b + d) * 4 + c * 6) >> 4;
    // ...  

另一个技巧是“缓存” src 的值,这样每次只添加一个新的值,因为src[index+2] 中的值最多可以使用 5 次。

所以这里是一个概念的例子:

//horizontal  filter
for(unsigned y = 0; y < height;  y++)
{
    int index = y*width + 2;
    register YOUR_DATA_TYPE a, b, c, d, e;
    a = src[index - 2].r;
    b = src[index - 1].r;
    c = src[index + 0].r; // The " + 0" is to show a pattern.
    d = src[index + 1].r;
    e = src[index + 2].r;
    for(unsigned x = 2; x < width-2;  x++)
    {
        dest[index - 2 + x].r = (a + e + (b + d) * 4 + c * 6) >> 4;
        a = b;
        b = c;
        c = d;
        d = e;
        e = src[index + x].r;

【讨论】:

    【解决方案2】:

    我不确定你的编译器会如何优化这一切,但我倾向于使用指针。假设您的结构是 3 个字节......您可以从正确位置的指针开始(源的过滤器边缘和目标的目标),然后使用常量数组偏移量移动它们。我还在外部循环中添加了一个可选的 OpenMP 指令,因为这也可以改进。

    #pragma omp parallel for
    for(unsigned y = 0; y < height;  y++) {
        const int rowindex = y * width;
        char * dpos = (char*)&dest[rowindex+2];
        char * spos = (char*)&src[rowindex];
        const char *end = (char*)&src[rowindex+width-2];
    
        for( ; spos != end;  spos++, dpos++) {
            *dpos = (spos[0] + spos[4] + ((spos[1] + src[3])<<2) + spos[2]*6) >> 4;
        }
    }
    

    垂直循环也是如此。

    const int scanwidth = width * 3;
    const int row1 = scanwidth;
    const int row2 = row1+scanwidth;
    const int row3 = row2+scanwidth;
    const int row4 = row3+scanwidth;
    
    #pragma omp parallel for
    for(unsigned y = 2;  y < height-2;  y++) {
        const int rowindex = y * width;
        char * dpos = (char*)&dest[rowindex];
        char * spos = (char*)&src[rowindex-row2];
        const char *end = spos + scanwidth;
    
        for( ; spos != end;  spos++, dpos++) {
            *dpos = (spos[0] + spos[row4] + ((spos[row1] + src[row3])<<2) + spos[row2]*6) >> 4;
        }
    }
    

    无论如何,这就是我进行卷积的方式。它稍微牺牲了可读性,而且我从未尝试过测量差异。我只是倾向于从一开始就这样写。看看这是否能让你加速。如果你有一台多核机器,OpenMP 肯定会,而且指针的东西可能

    我喜欢关于使用 SSE 进行这些操作的评论。

    【讨论】:

    • 您还可以考虑在源和目标指针上使用restrict 关键字来告诉编译器它们不会重叠。我不确定这在这里是否重要。老实说,我经常对这个关键字有点困惑。 =)
    【解决方案3】:

    一些更明显的优化是利用内核的对称性:

    a=*src++;    b=*src++;    c=*src++;    d=*src++;    e=*src++; // init
    
    LOOP (n/5) times:
    z=(a+e)+(b+d)<<2+c*6;  *dst++=z>>4;  // then reuse the local variables
    a=*src++;
    z=(b+a)+(c+e)<<2+d*6;  *dst++=z>>4;  // registers have been read only once...
    b=*src++;
    z=(c+b)+(d+a)<<2+e*6;  *dst++=z>>4;
    e=*src++;
    

    第二件事是可以使用一个整数执行多次加法。当要过滤的值是无符号的时,可以在一个 32 位整数中容纳两个通道(或在 64 位整数中容纳 4 个通道);这是穷人的 SIMD。

    a=  0x[0011][0034]  <-- split to two 
    b=  0x[0031][008a]
    ----------------------
    sum    0042  00b0
    >>4    0004  200b0  <-- mask off
    mask   00ff  00ff   
    -------------------
           0004  000b   <-- result 
    

    (模拟 SIMD 显示一个加法,然后移位 4)

    这是一个并行计算 3 个 rgb 操作的内核(在 64 位架构中很容易修改为 6 个 rgb 操作...)

    #define MASK (255+(255<<10)+(255<<20))
    #define KERNEL(a,b,c,d,e) { \
     a=((a+e+(c<<1))>>2) & MASK; a=(a+b+c+d)>>2 & MASK; *DATA++ = a; a=DATA[4]; }
    
    void calc_5_rgbs(unsigned int *DATA)
    {
       register unsigned int a = DATA[0], b=DATA[1], c=DATA[2], d=DATA[3], e=DATA[4];
       KERNEL(a,b,c,d,e);
       KERNEL(b,c,d,e,a);
       KERNEL(c,d,e,a,b);
       KERNEL(d,e,a,b,c);
       KERNEL(e,a,b,c,d);
    }
    

    在 ARM 和具有 16 个寄存器的 64 位 IA 上效果最佳...需要大量的汇编程序优化以克服 32 位 IA 中的寄存器短缺问题(例如,使用 ebp 作为 GPR)。正因为如此,它是一个就地算法......

    每 8 位数据之间只有 2 个保护位,这足以得到与整数计算完全相同的结果。

    顺便说一句:仅逐字节遍历数组字节比遍历 r,g,b 元素要快

     unsigned char *s=(unsigned char *) source_array; 
     unsigned char *d=(unsigned char *) dest_array; 
     for (j=0;j<3*N;j++) d[j]=(s[j]+s[j+16]+s[j+8]*6+s[j+4]*4+s[j+12]*4)>>4;
    

    【讨论】:

    • 只有一个可以将三个通道适配为 32 位(精度降低/舍入)
    • 我会收回的。对准确性没有损害...在 32 位 IA 中,一个很容易用完寄存器,因此速度优势几乎没有。 (最高 75% ...)
    猜你喜欢
    • 2015-01-08
    • 2023-04-03
    • 2018-10-03
    • 1970-01-01
    • 2013-03-29
    • 1970-01-01
    • 2018-01-26
    • 2013-02-01
    • 2014-10-02
    相关资源
    最近更新 更多