2012-09-25 7 views
5

Odnosząc się do odpowiedzi @ auselen tutaj: Using ARM NEON intrinsics to add alpha and permute, wygląda na to, że kompilator Armcc jest o wiele lepszy niż kompilator gcc do optymalizacji NEON. Czy to naprawdę prawda? Naprawdę nie próbowałem kompilatora Armcc. Ale mam dość zoptymalizowany kod przy użyciu kompilatora gcc z flagą optymalizacji -O3. Ale teraz zastanawiam się, czy armcc jest naprawdę tak dobry? Więc który z dwóch kompilatorów jest lepszy, biorąc pod uwagę wszystkie czynniki?Który z nich jest lepszy, gcc lub armcc dla optymalizacji NEON?

+5

Obsługa NEON w gcc jest mniej dojrzała niż skalarna obsługa liczb całkowitych/fp. Jednak porównanie Auselena opiera się na gcc 4.4.3, wydanym ponad 2,5 roku temu. Od tamtej pory sporo wysiłków poszło na ulepszenia NEON. W tym samym czasie armcc 5.01 ma zaledwie rok. Chociaż nadal oczekiwałbym, że Armcc 5.02 będzie na czele, bardziej odpowiednie porównanie będzie pomiędzy nim a 4,7 gcc. – unixsmurf

+1

@unixsmurf jeden z +1 jest przeze mnie :) – auselen

Odpowiedz

7

Kompilatory są również oprogramowaniem, z czasem ulegają poprawie. Wszelkie ogólne twierdzenia, takie jak armcc, są lepsze niż GCC na NEON (lub lepiej powiedziane jako wektoryzacja) nie mogą trwać prawdziwie wiecznie, ponieważ jedna grupa deweloperów może z wystarczającą uwagą zamknąć lukę. Jednak początkowo logiczne jest oczekiwanie, że kompilatory opracowane przez firmy sprzętowe będą lepsze, ponieważ muszą demonstrować/sprzedawać te funkcje.

Jeden z ostatnich przykładów, który widziałem, był tutaj w Stack Overflow na temat answer for branch prediction. Cytowanie z ostatniej linii zaktualizowanej sekcji "To pokazuje, że nawet dojrzałe współczesne kompilatory mogą bardzo się różnić w zakresie możliwości optymalizacji kodu ...".

Jestem wielkim fanem GCC, ale nie postawiłbym na jakość kodu produkowanego przez niego przeciwko kompilatorom z Intel lub ARM.Oczekuję, że każdy komercyjny kompilator głównego formatu będzie produkował kod co najmniej tak dobry, jak GCC.

Jedną z empirycznych odpowiedzi na to pytanie może być użycie hilbert-space's neon optimization example i zobaczenie, jak różne kompilatory ją optymalizują.

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; 
    } 
} 

To 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> 

Jest 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> 

który wygląda bardzo podobnie, więc mamy remis. Po obejrzeniu tego próbowałem wspomnieć o ponownym dodawaniu alfa i permutacji.

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; 
    } 
} 

Kompilowanie z 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 

Kompilacja z 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 

W tym przypadku armcc produkuje znacznie lepszy kod. Myślę, że to usprawiedliwia fgp's answer above. W większości przypadków GCC wytworzy wystarczająco dobry kod, ale powinieneś mieć oko na najważniejsze części lub, co najważniejsze, najpierw musisz zmierzyć/profilować.

+1

Spróbuj użyć flagi "-marm" w GCC, kod kciuka nie jest tak dojrzały w GCC, jak jeszcze, jeszcze bardziej dla jednostki thumb2 w Cortex-A9. – sgupta

+1

@ user1075375 updated. – auselen

+2

Hmm, zgodnie z oczekiwaniami, rejestracja rozlewania jest dość rozległa w przypadku gcc. – sgupta

7

Jeśli używasz wewnętrznej architektury NEON, kompilator nie powinien mieć aż takiego znaczenia. Większość (jeśli nie wszystkie) elementów wewnętrznych NEON jest tłumaczonych na pojedynczą instrukcję NEON, więc jedyną rzeczą pozostającą do kompilatora jest przydzielanie rejestrów i planowanie instrukcji. Z mojego doświadczenia wynika, że ​​zarówno GCC 4.2, jak i Clang 3.1 radzą sobie całkiem nieźle.

Należy jednak pamiętać, że instrukcje NEON są nieco bardziej wyraziste niż instrukcje NEON. Na przykład instrukcje ładowania/zapisywania NEON mają tryby adressing przed i po inkrementacji, które łączą obciążenie lub magazyn z przyrostem rejestru adresów, oszczędzając w ten sposób jedną instrukcję. Wewnętrzny NEON nie zapewnia wyraźnego sposobu, aby to zrobić, ale zamiast tego polegać na kompilatorze, aby połączyć regulator NEON load/store internal i przyrost adresu do instrukcji load/store z post-increment. Podobnie, niektóre instrukcje ładowania/przechowywania pozwalają określić wyrównanie adresu pamięci i wykonać szybciej, jeśli określone zostaną surowsze gwarancje wyrównania. Instancja NEON, ponownie, nie pozwala ci wyraźnie określić wyrównania, ale zamiast tego polegać na kompilatorze, aby wydedukować poprawny specyfikator wyrównania. Teoretycznie używasz "wyrównania" atrybutów na twoich wskazówkach, aby dostarczyć odpowiednich wskazówek do kompilatora, ale przynajmniej Clang zdaje się ignorować te ...

Z mojego doświadczenia, ani Clang, ani GCC nie są zbyt jasne, jeśli chodzi o tego rodzaju optymalizacje. Na szczęście dodatkowa korzyść wynikająca z takiej optymalizacji zwykle nie jest aż tak wysoka - to więcej niż 10% niż 100%.

Innym obszarem, w którym te dwa kompilatory nie są szczególnie inteligentne, jest unikanie rozrzucania stosu. Jeśli używasz więcej zmiennych o wartości wektorowej niż w rejestrach NEON, wydaje mi się, że oba kompilatory generują okropny kod. Zasadniczo, wydaje im się, że należy zaplanować instrukcje oparte na założeniu, że dostępna jest wystarczająca ilość rejestrów. Alokacja rejestru wydaje się następować później i wydaje się po prostu rozlewać wartości na stos po uruchomieniu rejestrów. Dlatego upewnij się, że kod ma działający zestaw mniej niż 16 wektory 128-bitowe lub 32 64-bitowy w dowolnym momencie!

Ogólnie rzecz biorąc, mam całkiem dobre wyniki zarówno z GCC, jak i Clanga, ale regularnie musiałem trochę reorganizować kod, aby uniknąć kompilacji Idiosyncrazji. Moja rada to trzymanie się GCC lub Clang, ale regularnie sprawdzaj z wybranym przez siebie disassemblerem.

Tak więc, ogólnie mówiąc, trzymanie się GCC jest w porządku. Możesz jednak rozważyć demontaż części krytycznych pod względem wydajności i sprawdzić, czy wygląda rozsądnie.

Powiązane problemy