0

我想改进下一个代码,计算平均值:

void calculateMeanStDev8x8Aux(cv::Mat* patch, int sx, int sy, int& mean, float& stdev)
{

    unsigned sum=0;
    unsigned sqsum=0;
    const unsigned char* aux=patch->data + sy*patch->step + sx;
    for (int j=0; j< 8; j++) {
        const unsigned char* p = (const unsigned char*)(j*patch->step + aux ); //Apuntador al inicio de la matrix           

        for (int i=0; i<8; i++) {
            unsigned f = *p++;
            sum += f;
            sqsum += f*f;
        }           
    }       

    mean = sum >> 6;
    int r = (sum*sum) >> 6;
    stdev = sqrtf(sqsum - r);

    if (stdev < .1) {
        stdev=0;
    }
}

我还使用 NEON 内在函数改进了下一个循环:

 for (int i=0; i<8; i++) {
            unsigned f = *p++;
            sum += f;
            sqsum += f*f;
        }

这是为另一个循环改进的代码:

        int32x4_t vsum= { 0 };
        int32x4_t vsum2= { 0 };

        int32x4_t vsumll = { 0 };
        int32x4_t vsumlh = { 0 };
        int32x4_t vsumll2 = { 0 };
        int32x4_t vsumlh2 = { 0 };

        uint8x8_t  f= vld1_u8(p); // VLD1.8 {d0}, [r0]

        //int 16 bytes /8 elementos
        int16x8_t val =  (int16x8_t)vmovl_u8(f);

        //int 32 /4 elementos *2 
        int32x4_t vall = vmovl_s16(vget_low_s16(val));
        int32x4_t valh = vmovl_s16(vget_high_s16(val));

        // update 4 partial sum of products vectors

        vsumll2 = vmlaq_s32(vsumll2, vall, vall);
        vsumlh2 = vmlaq_s32(vsumlh2, valh, valh);

        // sum 4 partial sum of product vectors
        vsum = vaddq_s32(vall, valh);
        vsum2 = vaddq_s32(vsumll2, vsumlh2);

        // do scalar horizontal sum across final vector

        sum += vgetq_lane_s32(vsum, 0);
        sum += vgetq_lane_s32(vsum, 1);
        sum += vgetq_lane_s32(vsum, 2);
        sum += vgetq_lane_s32(vsum, 3);

        sqsum += vgetq_lane_s32(vsum2, 0);
        sqsum += vgetq_lane_s32(vsum2, 1);
        sqsum += vgetq_lane_s32(vsum2, 2);
        sqsum += vgetq_lane_s32(vsum2, 3);

但它或多或少慢了 30 毫秒。有谁知道为什么?

所有代码都工作正常。

4

4 回答 4

3

添加到伦丁。是的,像 ARM 这样的指令集,你有一个基于寄存器的索引或一些直接索引,你可能会受益于鼓励编译器使用索引。此外,例如 ARM 可以在加载指令中增加其指针寄存器,基本上 *p++ 在一条指令中。

使用 p[i] 或 p[i++] 与 *p 或 *p++ 总是一个折腾,一些指令集更明显地采用哪条路径。

同样你的索引。如果你不使用它倒计时而不是倒计时可以节省每个循环的指令,也许更多。有些人可能会这样做:

inc reg
cmp reg,#7
bne loop_top

如果您正在倒计时,但您可能会在每个循环中保存一条指令:

dec reg
bne loop_top

甚至我知道的一个处理器

decrement_and_jump_if_not_zero  loop_top

编译器通常知道这一点,您不必鼓励他们。但是,如果您使用 p[i] 形式的内存读取顺序很重要,那么编译器不能或至少不应该任意更改读取顺序。因此,对于这种情况,您可能希望代码倒计时。

所以我尝试了所有这些事情:

unsigned fun1 ( const unsigned char *p, unsigned *x )
{
    unsigned sum;
    unsigned sqsum;
    int i;
    unsigned f;


    sum = 0;
    sqsum = 0;
    for(i=0; i<8; i++)
    {
        f = *p++;
        sum += f;
        sqsum += f*f;
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

unsigned fun2 ( const unsigned char *p, unsigned *x  )
{
    unsigned sum;
    unsigned sqsum;
    int i;
    unsigned f;


    sum = 0;
    sqsum = 0;
    for(i=8;i--;)
    {
        f = *p++;
        sum += f;
        sqsum += f*f;
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

unsigned fun3 ( const unsigned char *p, unsigned *x )
{
    unsigned sum;
    unsigned sqsum;
    int i;

    sum = 0;
    sqsum = 0;
    for(i=0; i<8; i++)
    {
        sum += (unsigned)p[i];
        sqsum += ((unsigned)p[i])*((unsigned)p[i]);
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

unsigned fun4 ( const unsigned char *p, unsigned *x )
{
    unsigned sum;
    unsigned sqsum;
    int i;

    sum = 0;
    sqsum = 0;
    for(i=8; i;i--)
    {
        sum += (unsigned)p[i-1];
        sqsum += ((unsigned)p[i-1])*((unsigned)p[i-1]);
    }
    //to keep the compiler from optimizing
    //stuff out
    x[0]=sum;
    return(sqsum);
}

同时使用 gcc 和 llvm (clang)。当然,两者都展开了循环,因为它是一个常数。gcc,为每个实验生成相同的代码,以防寄存器组合发生细微的变化。我会争辩一个错误,因为其中至少有一个错误不是按照代码描述的顺序进行的。

所有四个的 gcc 解决方案都是这样,有一些读取重新排序,请注意源代码中的读取是无序的。如果这违反了依赖于代码描述顺序的读取的硬件/逻辑,那么您将遇到大问题。

00000000 <fun1>:
   0:   e92d05f0    push    {r4, r5, r6, r7, r8, sl}
   4:   e5d06001    ldrb    r6, [r0, #1]
   8:   e00a0696    mul sl, r6, r6
   c:   e4d07001    ldrb    r7, [r0], #1
  10:   e02aa797    mla sl, r7, r7, sl
  14:   e5d05001    ldrb    r5, [r0, #1]
  18:   e02aa595    mla sl, r5, r5, sl
  1c:   e5d04002    ldrb    r4, [r0, #2]
  20:   e02aa494    mla sl, r4, r4, sl
  24:   e5d0c003    ldrb    ip, [r0, #3]
  28:   e02aac9c    mla sl, ip, ip, sl
  2c:   e5d02004    ldrb    r2, [r0, #4]
  30:   e02aa292    mla sl, r2, r2, sl
  34:   e5d03005    ldrb    r3, [r0, #5]
  38:   e02aa393    mla sl, r3, r3, sl
  3c:   e0876006    add r6, r7, r6
  40:   e0865005    add r5, r6, r5
  44:   e0854004    add r4, r5, r4
  48:   e5d00006    ldrb    r0, [r0, #6]
  4c:   e084c00c    add ip, r4, ip
  50:   e08c2002    add r2, ip, r2
  54:   e082c003    add ip, r2, r3
  58:   e023a090    mla r3, r0, r0, sl
  5c:   e080200c    add r2, r0, ip
  60:   e5812000    str r2, [r1]
  64:   e1a00003    mov r0, r3
  68:   e8bd05f0    pop {r4, r5, r6, r7, r8, sl}
  6c:   e12fff1e    bx  lr

加载索引和微妙的寄存器混合是 gcc 函数之间的唯一区别,所有操作都以相同的顺序进行。

llvm/clang:

00000000 <fun1>:
   0:   e92d41f0    push    {r4, r5, r6, r7, r8, lr}
   4:   e5d0e000    ldrb    lr, [r0]
   8:   e5d0c001    ldrb    ip, [r0, #1]
   c:   e5d03002    ldrb    r3, [r0, #2]
  10:   e5d08003    ldrb    r8, [r0, #3]
  14:   e5d04004    ldrb    r4, [r0, #4]
  18:   e5d05005    ldrb    r5, [r0, #5]
  1c:   e5d06006    ldrb    r6, [r0, #6]
  20:   e5d07007    ldrb    r7, [r0, #7]
  24:   e08c200e    add r2, ip, lr
  28:   e0832002    add r2, r3, r2
  2c:   e0882002    add r2, r8, r2
  30:   e0842002    add r2, r4, r2
  34:   e0852002    add r2, r5, r2
  38:   e0862002    add r2, r6, r2
  3c:   e0870002    add r0, r7, r2
  40:   e5810000    str r0, [r1]
  44:   e0010e9e    mul r1, lr, lr
  48:   e0201c9c    mla r0, ip, ip, r1
  4c:   e0210393    mla r1, r3, r3, r0
  50:   e0201898    mla r0, r8, r8, r1
  54:   e0210494    mla r1, r4, r4, r0
  58:   e0201595    mla r0, r5, r5, r1
  5c:   e0210696    mla r1, r6, r6, r0
  60:   e0201797    mla r0, r7, r7, r1
  64:   e8bd41f0    pop {r4, r5, r6, r7, r8, lr}
  68:   e1a0f00e    mov pc, lr

更容易阅读和遵循,也许考虑缓存并一次读取所有内容。至少在一种情况下,llvm 的读取也出现了乱序。

00000144 <fun4>:
 144:   e92d40f0    push    {r4, r5, r6, r7, lr}
 148:   e5d0c007    ldrb    ip, [r0, #7]
 14c:   e5d03006    ldrb    r3, [r0, #6]
 150:   e5d02005    ldrb    r2, [r0, #5]
 154:   e5d05004    ldrb    r5, [r0, #4]
 158:   e5d0e000    ldrb    lr, [r0]
 15c:   e5d04001    ldrb    r4, [r0, #1]
 160:   e5d06002    ldrb    r6, [r0, #2]
 164:   e5d00003    ldrb    r0, [r0, #3]

是的,为了对 ram 中的一些值进行平均,顺序不是问题,继续前进。

所以编译器选择展开的路径并且不关心微优化。由于循环的大小,两者都选择烧毁一堆寄存器,每个循环都保存一个加载的值,然后从这些临时读取或乘法中执行加法。如果我们稍微增加循环的大小,我希望在展开的循环中看到 sum 和 sqsum 累积,因为它用完了寄存器,否则将达到他们选择不展开循环的阈值。

如果我传入长度,并将上面代码中的 8 替换为传入的长度,则强制编译器对此进行循环。您可以看到优化,使用如下指令:

  a4:   e4d35001    ldrb    r5, [r3], #1

并且作为手臂,他们在一个地方对循环寄存器进行了修改,如果不等于稍后的指令数量则分支......因为他们可以。

当然这是一个数学函数,但使用浮点数很痛苦。并且使用乘法是痛苦的,分裂更糟,幸运的是使用了转变。幸运的是,这是无符号的,因此您可以使用移位(如果您对有符号数使用除法,编译器将/应该知道使用算术移位)。

所以基本上关注内部循环的微优化,因为它会运行多次,如果可以改变它,那么它会变成移位和添加,如果可能的话,或者安排数据以便你可以把它从循环中取出(如果可能的话,不要在其他地方浪费其他复制循环来执行此操作)

const unsigned char* p = (const unsigned char*)(j*patch->step + aux );

你可以得到一些速度。我没有尝试过,但因为它是循环中的循环,编译器可能不会展开该循环......

长话短说,您可能会根据针对笨拙编译器的指令集获得一些收益,但是这段代码并不是很糟糕,因此编译器可以尽可能地优化它。

于 2011-12-15T16:08:00.223 回答
1

首先,如果您在Code review上发帖,您可能会得到关于此类内容的非常好的、详细的答案。

关于效率和可疑变量类型的一些评论:

unsigned f = *p++;p如果您通过数组索引访问然后使用 p[i] 访问数据,您可能会更好。这高度依赖于编译器、缓存内存优化等(在这件事上,一些 ARM 专家可以提供比我更好的建议)。

顺便说一句,整个 const char 到 int 看起来非常可疑。我认为那些字符被视为 8 位无符号整数?标准 Cuint8_t可能是一种更好的类型,它char具有您想要避免的各种未定义的签名问题。

另外,你为什么要疯狂混合unsignedand int?您要求隐式整数平衡错误。

stdev < .1. 只是一件小事:将其更改为.1f或强制将浮点数隐式提升为双精度,因为 .1 是双精度文字。

于 2011-12-15T12:46:42.240 回答
1

由于您的数据是以 8 个字节为一组读取的,具体取决于您的硬件总线和数组本身的对齐方式,您可以通过一次长长读取读取内部循环,然后手动拆分数字转换为单独的值,或使用 ARM 内部函数使用 add8 指令与一些内联 asm 并行进行加法(在 1 个寄存器中一次将 4 个数字加在一起)或进行一些移位并使用 add16 允许值溢出进入 16 位的空间。还有一个双符号乘法和累加指令,只需一点帮助,您的第一个累加循环就可以通过 ARM 得到几乎完美的支持。此外,如果传入的数据可以被处理为 16 位值,那么也可以加快速度。

至于为什么 NEON 速度较慢,我的猜测是设置向量的开销以及您使用较大类型推送的添加数据正在扼杀它可能通过这么少的数据集获得的任何性能。原始代码一开始就对 ARM 非常友好,这意味着设置开销可能会害死你。如有疑问,请查看汇编输出。这将告诉你真正发生了什么。也许编译器在尝试使用内在函数时会到处推送和弹出数据——这不是我第一次看到这种行为。

于 2011-12-15T17:26:45.967 回答
0

感谢 Lundin、dwelch 和 Michel。我做了下一个改进,它似乎对我的代码是最好的。我正在尝试减少改善缓存访问的周期数,因为只访问一次缓存。

int step=patch->step;
 for (int j=0; j< 8; j++) {
        p = (uint8_t*)(j*step + aux ); /

        i=8;
        do {                
            f=p[i];
            sum += f;
            sqsum += f*f;

        } while(--i);

}
于 2011-12-16T11:52:08.903 回答