ここで@auselenの回答を参照してください:ARM NEON組み込み関数を使用してアルファと順列を追加すると、NEON最適化にはarmccコンパイラがgccコンパイラよりもはるかに優れているように見えます。これは本当に本当ですか?私は実際にarmccコンパイラを試していません。しかし、-O3最適化フラグを指定したgccコンパイラを使用して、かなり最適化されたコードを取得しました。しかし今、私はarmccが本当にそんなに良いのだろうかと思っていますか?では、すべての要因を考慮して、2つのコンパイラのどちらが優れているのでしょうか。
2 に答える
コンパイラもソフトウェアであり、時間の経過とともに向上する傾向があります。armccのような一般的な主張はNEONのGCCよりも優れています(またはベクトル化と呼ばれる方が良い)。1つの開発者グループが十分な注意を払ってギャップを埋めることができるため、永遠に当てはまるわけではありません。ただし、最初は、ハードウェア会社によって開発されたコンパイラがこれらの機能を実証/販売する必要があるため、優れていると期待するのは論理的です。
私が最近見た例の1つは、分岐予測の答えについてのStackOverflowに関するものです。更新されたセクションの最後の行からの引用「これは、成熟した最新のコンパイラーでさえ、コードを最適化する能力が大きく異なる可能性があることを示しています...」。
私はGCCの大ファンですが、IntelやARMのコンパイラーに対して、GCCによって生成されるコードの品質には賭けません。主流の商用コンパイラーは、少なくともGCCと同じくらい良いコードを生成することを期待しています。
この質問に対する経験的な答えの1つは、ヒルベルト空間のネオン最適化の例を使用して、さまざまなコンパイラがそれをどのように最適化するかを確認することです。
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;
}
}
これはarmcc5.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>
これはGCC4.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>
これは非常によく似ているので、引き分けになります。これを見た後、私は言及されたアルファを追加し、再び順列を試してみました。
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命令に変換されるため、コンパイラーに残されているのは、レジスタ割り当てと命令スケジューリングだけです。私の経験では、GCC4.2とClang3.1の両方がこれらのタスクでかなりうまく機能します。
ただし、NEON命令は、NEON本能よりも少し表現力が高いことに注意してください。たとえば、NEONのロード/ストア命令には、ロードまたはストアとアドレスレジスタのインクリメントを組み合わせたインクリメント前およびインクリメント後のアドレス指定モードがあり、1つの命令を節約できます。NEON組み込み関数は、それを行うための明示的な方法を提供しませんが、代わりにコンパイラに依存して、レギュラーNEONロード/ストア組み込み関数とアドレス増分をポストインクリメント付きのロード/ストア命令に結合します。同様に、一部のロード/ストア命令では、メモリアドレスのアラインメントを指定でき、より厳密なアラインメント保証を指定すると、より高速に実行できます。NEON組み込み関数でも、アライメントを明示的に指定することはできませんが、代わりにコンパイラに依存して正しいアライメント指定子を推測します。理論的には、「
私の経験では、これらの種類の最適化に関しては、ClangもGCCもあまり明るくありません。幸いなことに、これらの種類の最適化による追加のパフォーマンス上の利点は、通常、それほど高くはありません。100%よりも10%程度です。
これらの2つのコンパイラが特に賢くないもう1つの領域は、スタックの流出を回避することです。コードがNEONレジスタよりも多くのベクトル値変数を使用している場合、両方のコンパイラがひどいコードを生成しているように見えます。基本的に、彼らがしているように見えるのは、十分なレジスタが利用可能であるという仮定に基づいて命令をスケジュールすることです。レジスタ割り当ては後で行われるようであり、レジスタが実行されるとスタックに値をスピルするだけのようです。したがって、コードには、いつでも16個未満の128ビットベクトルまたは32個の64ビットベクトルのワーキングセットがあることを確認してください。
全体として、GCCとClangの両方からかなり良い結果が得られましたが、コンパイラーの特異性を回避するために、定期的にコードを少し再編成する必要がありました。私のアドバイスはGCCまたはClangに固執することですが、選択したディスアセンブラーで定期的にチェックしてください。
したがって、全体として、GCCに固執することは問題ないと思います。ただし、パフォーマンスが重要な部品の分解を確認し、それが妥当であるかどうかを確認することをお勧めします。