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



请参阅@auselen的答案:使用ARM NEON内部函数添加alpha和permute,在NEON优化方面,armcc编译器似乎比gcc编译器好得多。这真的是真的吗?我还没有真正尝试过armcc编译器。但我使用带有-O3优化标志的gcc编译器得到了非常优化的代码。但现在我想知道armcc是否真的那么好?那么,考虑到所有因素,两个编译器中哪一个更好呢?

编译器也是软件,它们往往会随着时间的推移而改进。任何像armcc这样的通用声明在NEON上都比GCC好(或者更好地说是矢量化),都不可能永远成立,因为一个开发小组可以足够关注地缩小差距。然而,最初期望硬件公司开发的编译器更出色是合乎逻辑的,因为它们需要展示/营销这些功能。

我最近在Stack Overflow上看到的一个例子是关于分支预测的答案。引用更新部分最后一行的话:"这表明,即使是成熟的现代编译器,在优化代码的能力上也会有很大的差异……">

我是GCC的忠实粉丝,但我不会把它产生的代码质量与英特尔或ARM的编译器相提并论。我希望任何主流的商业编译器都能产生至少和GCC一样好的代码。

这个问题的一个经验答案是使用hilbert空间的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会生成足够好的代码,但您应该关注关键部分,或者最重要的是,首先您必须测量/评测。

如果使用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,但要定期与您选择的disassembler联系。

所以,总的来说,我认为坚持GCC是好的。不过,您可能需要查看性能关键部件的分解,并检查它是否合理。

相关内容

  • 没有找到相关文章

最新更新