对于NEON优化,gcc和armcc哪个更好?

8
参考@auselen的答案:在这里使用ARM NEON内置函数添加alpha和排列,看起来armcc编译器比gcc编译器更适合NEON优化。这是真的吗?我还没有真正尝试过armcc编译器。但是我使用gcc编译器和-O3优化标志得到了非常优化的代码。但现在我想知道armcc是否真的那么好?所以从所有因素考虑,哪个编译器更好?

5
gcc对NEON的支持不如对标量整型/浮点数的支持成熟。然而,auselen的比较基于2.5年前发布的gcc 4.4.3。自那时以来,NEON的改进已经有了很多工作。同时,armcc 5.01只有一年的历史。虽然我仍然期望armcc 5.02更领先,但更相关的比较应该是与4.7版本的gcc进行比较。 - unixsmurf
1
@unixsmurf,我给你点赞了 :) - auselen
2个回答

11
编译器也是软件,随着时间推移它们往往会得到改进。任何一般性的说法,比如armcc在NEON上(或者更准确地说是向量化)比GCC好,都不能永远成立,因为只要有足够的关注,一个开发组就可以缩小差距。然而,最初可以合理地期望由硬件公司开发的编译器优于其他编译器,因为他们需要展示/营销这些功能。
我最近在Stack Overflow上看到了一个例子,关于branch prediction的答案。引用更新部分的最后一行:"这表明即使是现代成熟的编译器在优化代码方面也可能存在巨大差异..."
我是GCC的忠实粉丝,但我不会押注它产生的代码质量优于Intel或ARM的编译器。我期望任何主流商业编译器至少能够产生与GCC同样好的代码。
对于这个问题的一个经验性答案是使用hilbert-space的neon优化示例,看看不同的编译器如何进行优化。
void neon_convert (uint8_t * __restrict dest, uint8_t * __restrict src, int n)
{
  int i;
  uint8x8_t rfac = vdup_n_u8 (77);
  uint8x8_t gfac = vdup_n_u8 (151);
  uint8x8_t bfac = vdup_n_u8 (28);
  n/=8;

  for (i=0; i<n; i++)
  {
    uint16x8_t  temp;
    uint8x8x3_t rgb  = vld3_u8 (src);
    uint8x8_t result;

    temp = vmull_u8 (rgb.val[0],      rfac);
    temp = vmlal_u8 (temp,rgb.val[1], gfac);
    temp = vmlal_u8 (temp,rgb.val[2], bfac);

    result = vshrn_n_u16 (temp, 8);
    vst1_u8 (dest, result);
    src  += 8*3;
    dest += 8;
  }
}

这是 armcc 5.01

  20:   f421140d    vld3.8  {d1-d3}, [r1]!
  24:   e2822001    add r2, r2, #1
  28:   f3810c04    vmull.u8    q0, d1, d4
  2c:   f3820805    vmlal.u8    q0, d2, d5
  30:   f3830806    vmlal.u8    q0, d3, d6
  34:   f2880810    vshrn.i16   d0, q0, #8
  38:   f400070d    vst1.8  {d0}, [r0]!
  3c:   e1520003    cmp r2, r3
  40:   bafffff6    blt 20 <neon_convert+0x20>

这是GCC 4.4.3-4.7.1

  1e:   f961 040d   vld3.8  {d16-d18}, [r1]!
  22:   3301        adds    r3, #1
  24:   4293        cmp r3, r2
  26:   ffc0 4ca3   vmull.u8    q10, d16, d19
  2a:   ffc1 48a6   vmlal.u8    q10, d17, d22
  2e:   ffc2 48a7   vmlal.u8    q10, d18, d23
  32:   efc8 4834   vshrn.i16   d20, q10, #8
  36:   f940 470d   vst1.8  {d20}, [r0]!
  3a:   d1f0        bne.n   1e <neon_convert+0x1e>

这两者看起来非常相似,所以我们打平局。在看到这个结果后,我尝试了加入 alpha 和再次进行排列。

void neonPermuteRGBtoBGRA(unsigned char* src, unsigned char* dst, int numPix)
{
    numPix /= 8; //process 8 pixels at a time

    uint8x8_t alpha = vdup_n_u8 (0xff);

    for (int i=0; i<numPix; i++)
    {
        uint8x8x3_t rgb  = vld3_u8 (src);
        uint8x8x4_t bgra;

        bgra.val[0] = rgb.val[2]; //these lines are slow
        bgra.val[1] = rgb.val[1]; //these lines are slow 
        bgra.val[2] = rgb.val[0]; //these lines are slow

        bgra.val[3] = alpha;

        vst4_u8(dst, bgra);

        src += 8*3;
        dst += 8*4;
    }
}

使用gcc编译...

$ arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-gcc (crosstool-NG linaro-1.13.1-2012.05-20120523 - Linaro GCC 2012.05) 4.7.1 20120514 (prerelease)
$ arm-linux-gnueabihf-gcc -std=c99 -O3 -c ~/temp/permute.c -marm -mfpu=neon-vfpv4 -mcpu=cortex-a9 -o ~/temp/permute_gcc.o

00000000 <neonPermuteRGBtoBGRA>:
   0:   e3520000    cmp r2, #0
   4:   e2823007    add r3, r2, #7
   8:   b1a02003    movlt   r2, r3
   c:   e92d01f0    push    {r4, r5, r6, r7, r8}
  10:   e1a021c2    asr r2, r2, #3
  14:   e24dd01c    sub sp, sp, #28
  18:   e3520000    cmp r2, #0
  1c:   da000019    ble 88 <neonPermuteRGBtoBGRA+0x88>
  20:   e3a03000    mov r3, #0
  24:   f460040d    vld3.8  {d16-d18}, [r0]!
  28:   eccd0b06    vstmia  sp, {d16-d18}
  2c:   e59dc014    ldr ip, [sp, #20]
  30:   e2833001    add r3, r3, #1
  34:   e59d6010    ldr r6, [sp, #16]
  38:   e1530002    cmp r3, r2
  3c:   e59d8008    ldr r8, [sp, #8]
  40:   e1a0500c    mov r5, ip
  44:   e59dc00c    ldr ip, [sp, #12]
  48:   e1a04006    mov r4, r6
  4c:   f3c73e1f    vmov.i8 d19, #255   ; 0xff
  50:   e1a06008    mov r6, r8
  54:   e59d8000    ldr r8, [sp]
  58:   e1a0700c    mov r7, ip
  5c:   e59dc004    ldr ip, [sp, #4]
  60:   ec454b34    vmov    d20, r4, r5
  64:   e1a04008    mov r4, r8
  68:   f26401b4    vorr    d16, d20, d20
  6c:   e1a0500c    mov r5, ip
  70:   ec476b35    vmov    d21, r6, r7
  74:   f26511b5    vorr    d17, d21, d21
  78:   ec454b34    vmov    d20, r4, r5
  7c:   f26421b4    vorr    d18, d20, d20
  80:   f441000d    vst4.8  {d16-d19}, [r1]!
  84:   1affffe6    bne 24 <neonPermuteRGBtoBGRA+0x24>
  88:   e28dd01c    add sp, sp, #28
  8c:   e8bd01f0    pop {r4, r5, r6, r7, r8}
  90:   e12fff1e    bx  lr

使用 armcc 进行编译...

$ armcc
ARM C/C++ Compiler, 5.01 [Build 113]
$ armcc --C99 --cpu=Cortex-A9 -O3 -c permute.c -o permute_arm.o

00000000 <neonPermuteRGBtoBGRA>:
   0:   e1a03fc2    asr r3, r2, #31
   4:   f3870e1f    vmov.i8 d0, #255    ; 0xff
   8:   e0822ea3    add r2, r2, r3, lsr #29
   c:   e1a031c2    asr r3, r2, #3
  10:   e3a02000    mov r2, #0
  14:   ea000006    b   34 <neonPermuteRGBtoBGRA+0x34>
  18:   f420440d    vld3.8  {d4-d6}, [r0]!
  1c:   e2822001    add r2, r2, #1
  20:   eeb01b45    vmov.f64    d1, d5
  24:   eeb02b46    vmov.f64    d2, d6
  28:   eeb05b40    vmov.f64    d5, d0
  2c:   eeb03b41    vmov.f64    d3, d1
  30:   f401200d    vst4.8  {d2-d5}, [r1]!
  34:   e1520003    cmp r2, r3
  38:   bafffff6    blt 18 <neonPermuteRGBtoBGRA+0x18>
  3c:   e12fff1e    bx  lr

在这种情况下,armcc 会生成更好的代码。我认为这证明了 fgp 上面的答案 的正确性。大多数时候,GCC 会生成足够好的代码,但你应该关注关键部分,或者更重要的是首先需要测量/剖析。

1
尝试在GCC中使用“-marm”标志,因为目前GCC中的thumb代码并不是很成熟,对于Cortex-A9中的thumb2单元更是如此。 - sgupta
2
哦,果然,gcc 中的寄存器溢出非常广泛。 - sgupta

7
如果您使用NEON内置函数,编译器的重要性就不是那么大了。大多数(如果不是全部)NEON内置函数都转换为单个NEON指令,因此留给编译器的唯一事情就是寄存器分配和指令调度。根据我的经验,GCC 4.2和Clang 3.1在这些任务上都做得相当好。
但是请注意,NEON指令比NEON内置函数更具表现力。例如,NEON加载/存储指令具有预增量和后增量寻址模式,它们将加载或存储与地址寄存器的增量组合起来,从而为您节省一条指令。 NEON内置函数没有提供明确的方法来执行此操作,而是依赖于编译器将常规NEON加载/存储内置函数和地址增量组合成带有后增量的加载/存储指令。同样,某些加载/存储指令允许您指定内存地址的对齐方式,并且如果您指定更严格的对齐保证,则执行速度更快。 NEON内置函数再次不允许您明确指定对齐方式,而是依赖于编译器推断正确的对齐说明符。理论上,您可以在指针上使用“align”属性来提供适当的提示,但至少Clang似乎忽略了这些...
根据我的经验,当涉及到这些类型的优化时,Clang和GCC都不是特别聪明。幸运的是,这些优化的额外性能收益通常并不是非常高-它更像是10%而不是100%。
另一个这两个编译器不太聪明的领域是避免堆栈溢出。如果您的代码使用的矢量值变量比NEON寄存器多,我看到这两个编译器都会产生可怕的代码。基本上,它们似乎是基于有足够的寄存器可用的假设来安排指令。似乎寄存器分配是在之后进行的,并且一旦运行寄存器就会将值溢出到堆栈中。因此,请确保您的代码在任何时候都具有小于16个128位向量或32个64位向量的工作集!
总体而言,我从GCC和Clang中获得了相当好的结果,但我经常不得不稍微重新组织代码以避免编译器的怪癖。我的建议是坚持使用GCC或Clang,但是请使用您选择的反汇编程序定期检查。
因此,总的来说,我认为坚持使用GCC是可以的。但是,您可能需要查看性能关键部分的反汇编代码,并检查它是否合理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接