为啥这段代码效率不高?

Posted

技术标签:

【中文标题】为啥这段代码效率不高?【英文标题】:Why this code is not efficient?为什么这段代码效率不高? 【发布时间】:2011-12-15 11:42:32 【问题描述】:

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

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 毫秒。有谁知道为什么?

所有代码都运行正常。

【问题讨论】:

有很多因素会影响性能(假设您以正确的方式测量处理时间)。显然你正在使用 OpenCV,所以我会说图像的大小有很大的不同。有多大? 单位“mms”是什么意思?是“毫秒”吗? “毫米秒”? 为什么标记为 C?它是 C++。 cv::Mat 您是否将其用于实时流数据? 图片是320X240,是的,我是实时使用的,所以帧的时间不同 【参考方案1】:

添加到伦丁。是的,像 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

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

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

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

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

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

长话短说,根据指令集与笨拙的编译器相比,您可能会有所收获,但这段代码并不是很糟糕,因此编译器可以尽可能地优化它。

【讨论】:

【参考方案2】:

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

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

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

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

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

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

【讨论】:

【参考方案3】:

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

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

【讨论】:

【参考方案4】:

感谢 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);


【讨论】:

以上是关于为啥这段代码效率不高?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用单个赋值运算符处理复制和移动赋值效率不高?

为啥这段代码不能访问'borderWidth'(附上iphone代码)

为啥.NET BASE64编码效率这么低?

为啥这段代码不抛出 NullPointerException?

为啥这段代码不会导致无限循环?

为啥这段代码不播放声音文件