2012-09-25 16 views
5

Refiriéndose a la respuesta de @auselen aquí: Using ARM NEON intrinsics to add alpha and permute, parece que el compilador de armcc es mucho mejor que el compilador de gcc para las optimizaciones de NEON. ¿Es esto realmente cierto? Realmente no he probado el compilador de armcc. Pero obtuve un código bastante optimizado usando el compilador gcc con el indicador de optimización -O3. Pero ahora me pregunto si armcc realmente es tan bueno. Entonces, ¿cuál de los dos compiladores es mejor, teniendo en cuenta todos los factores?¿Cuál es mejor, gcc o armcc para las optimizaciones de NEON?

+5

El soporte de neón en gcc es menos maduro que el apoyo número entero/fp escalar. Sin embargo, la comparación de auselen se basa en gcc 4.4.3, lanzado hace más de 2.5 años. Desde entonces, se ha invertido bastante trabajo en las mejoras de NEON. Al mismo tiempo, el brazo 5.01 tiene solo un año. Si bien aún esperaría que el brazo 5.02 estuviera adelante, una comparación más relevante sería entre él y un 4.7 gcc. – unixsmurf

+1

@unixsmurf uno de +1 es por mí :) – auselen

Respuesta

7

Los compiladores también son software, tienden a mejorar con el tiempo. Cualquier afirmación genérica como el brazo es mejor que GCC en NEON (o mejor dicho como vectorización) no puede ser verdad para siempre ya que un grupo de desarrolladores puede cerrar la brecha con suficiente atención. Sin embargo, inicialmente es lógico esperar que los compiladores desarrollados por las compañías de hardware sean superiores porque necesitan demostrar/comercializar estas características.

Un ejemplo reciente que vi fue aquí en Stack Overflow sobre un answer for branch prediction. Citando de la última línea de la sección actualizada "Esto demuestra que incluso los compiladores modernos maduros pueden variar enormemente en su capacidad para optimizar el código ...".

Soy un gran admirador de GCC, pero no apostaría a la calidad del código producido por él contra los compiladores de Intel o ARM.Espero que cualquier compilador comercial convencional produzca código al menos tan bueno como GCC.

Una respuesta empírica a esta pregunta podría ser el uso de hilbert-space's neon optimization example y ver cómo los diferentes compiladores optimizarlo.

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

Ésta es 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> 

Esta es 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> 

que se ve muy similares, por lo que tenemos un empate. Después de ver esto intenté agregar alpha y permute nuevamente.

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

compilación con 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 

Compilar con 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 

En este caso armcc produce mucho mejor código. Creo que esto justifica fgp's answer above. La mayoría de las veces, GCC producirá un código lo suficientemente bueno, pero debe vigilar las partes críticas o, lo que es más importante, primero debe medir/perfilar.

+1

Pruebe usar el indicador '-marm' en GCC, el código del pulgar no está tan maduro en GCC hasta el momento, más aún para la unidad thumb2 en Cortex-A9. – sgupta

+1

@ user1075375 actualizado. – auselen

+2

Hmm, como era de esperar, el filtrado de registros es bastante extenso con gcc. – sgupta

7

Si utiliza los intrínsecos de NEON, el compilador no debería importar demasiado. La mayoría (si no todos) los intrínsecos de NEON se traducen en una única instrucción NEON, por lo que lo único que le queda al compilador es la asignación de registros y la programación de instrucciones. En mi experiencia, tanto GCC 4.2 como Clang 3.1 funcionan razonablemente bien en esas tareas.

Tenga en cuenta, sin embargo, que las instrucciones NEON son un poco más expresivas que las instrinsics de NEON. Por ejemplo, las instrucciones de carga/almacenamiento de NEON tienen modos de direccionamiento de incremento previo y posterior que combinan una carga o almacenamiento con un incremento del registro de direcciones, lo que le ahorra una instrucción. Los intrínsecos de NEON no proporcionan una forma explícita de hacerlo, sino que confían en el compilador para combinar un regulador NEON load/store intrinsic y un incremento de dirección en una instrucción load/store con incremento posterior. De forma similar, algunas instrucciones de carga/almacenamiento le permiten especificar la alineación de la dirección de memoria y ejecutarla más rápido si especifica garantías de alineación más estrictas. Los intrínsecos de NEON, de nuevo, no le permiten especificar la alineación explícitamente, sino que confían en el compilador para deducir el especificador de alineación correcto. En teoría, se utiliza "align" atributos en sus punteros para ofrecer consejos adecuados para el compilador, pero al menos Clang parece ignorar los ...

En mi experiencia, ni tampoco Clang GCC son muy brillantes cuando se trata de ese tipo de optimizaciones. Afortunadamente, el beneficio de rendimiento adicional de este tipo de optimización generalmente no es tan alto: se parece más al 10% que al 100%.

Otra área donde esos dos compiladores no son especialmente inteligentes es evitar el derrame de la pila. Si el código utiliza más variables de valor vectorial que registros NEON, parece que ambos compiladores producen código horrible. Básicamente, lo que parecen hacer es programar instrucciones basadas en la suposición de que hay suficientes registros disponibles. La asignación de registros parece venir después, y parece simplemente derramar valores en la pila una vez que se ejecuta de registros. ¡Así que asegúrese de que el código tenga un conjunto de trabajo de menos de 16 vectores de 128 bits o 32 vectores de 64 bits en cualquier momento!

En general, obtuve muy buenos resultados tanto de GCC como de Clang, pero regularmente tuve que reorganizar un poco el código para evitar las idiosincrasias del compilador. Mi consejo sería que te quedes con GCC o Clang, pero revisa regularmente con el disensembler que elijas.

Así que, en general, diría que seguir con GCC está bien. Sin embargo, es posible que desee ver el desmontaje de las piezas críticas para el rendimiento y verificar si se ve razonable.

Cuestiones relacionadas