¿Por qué ARM NEON no es más rápido que plain C++?


Aquí hay un código C++:

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    for ( register int i = 0; i < ARR_SIZE_TEST; ++i )
    {
        x[ i ] = x[ i ] + y[ i ];
    }
}

Aquí hay una versión de neón:

void neon_assm_tst_add( unsigned* x, unsigned* y )
{
    register unsigned i = ARR_SIZE_TEST >> 2;

    __asm__ __volatile__
    (
        ".loop1:                            \n\t"

        "vld1.32   {q0}, [%[x]]             \n\t"
        "vld1.32   {q1}, [%[y]]!            \n\t"

        "vadd.i32  q0 ,q0, q1               \n\t"
        "vst1.32   {q0}, [%[x]]!            \n\t"

        "subs     %[i], %[i], $1            \n\t"
        "bne      .loop1                    \n\t"

        : [x]"+r"(x), [y]"+r"(y), [i]"+r"(i)
        :
        : "memory"
    );
}

Función de prueba:

void bench_simple_types_test( )
{
    unsigned* a = new unsigned [ ARR_SIZE_TEST ];
    unsigned* b = new unsigned [ ARR_SIZE_TEST ];

    neon_tst_add( a, b );
    neon_assm_tst_add( a, b );
}

He probado ambas variantes y aquí hay un informe:

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 185 ms // SLOW!!!

También probé otros tipos:

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms // FASTER X3!

LA PREGUNTA: ¿Por qué neon es más lento con tipos enteros de 32 bits?

Usé la última versión de GCC para Android NDK. Las banderas de optimización de NEÓN se encendieron. Aquí hay una versión desmontada de C++:

                 MOVS            R3, #0
                 PUSH            {R4}

 loc_8
                 LDR             R4, [R0,R3]
                 LDR             R2, [R1,R3]
                 ADDS            R2, R4, R2
                 STR             R2, [R0,R3]
                 ADDS            R3, #4
                 CMP.W           R3, #0x2000000
                 BNE             loc_8
                 POP             {R4}
                 BX              LR

Aquí está la versión desmontada de neon:

                 MOV.W           R3, #0x200000
.loop1
                 VLD1.32         {D0-D1}, [R0]
                 VLD1.32         {D2-D3}, [R1]!
                 VADD.I32        Q0, Q0, Q1
                 VST1.32         {D0-D1}, [R0]!
                 SUBS            R3, #1
                 BNE             .loop1
                 BX              LR

Aquí están todas las pruebas de banco:

add, char,     C++       : 83  ms
add, char,     neon asm  : 46  ms FASTER x2

add, short,    C++       : 114 ms
add, short,    neon asm  : 92  ms FASTER x1.25

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 184 ms SLOWER!!!

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms FASTER x3

add, double,   C++       : 533 ms
add, double,   neon asm  : 420 ms FASTER x1.25

LA PREGUNTA: ¿Por qué neon es más lento con tipos enteros de 32 bits?

Author: Smalti, 2011-04-20

5 answers

La canalización NEON en Cortex-A8 está en orden de ejecución, y tiene un hit-under-miss limitado (sin cambio de nombre), por lo que está limitado por la latencia de memoria (ya que está utilizando más de tamaño de caché L1/L2). Su código tiene dependencias inmediatas en los valores cargados desde la memoria, por lo que se detendrá constantemente esperando memoria. Esto explicaría por qué el código de NEÓN es ligeramente (en una pequeña cantidad) más lento que el no-NEÓN.

Es necesario desenrollar los bucles de montaje y aumentar la distancia entre la carga y el uso, por ejemplo:

vld1.32   {q0}, [%[x]]!
vld1.32   {q1}, [%[y]]!
vld1.32   {q2}, [%[x]]!
vld1.32   {q3}, [%[y]]!
vadd.i32  q0 ,q0, q1
vadd.i32  q2 ,q2, q3
...

Hay un montón de registros de neón para que pueda desenrollarlo mucho. El código entero sufrirá el mismo problema, en menor medida porque el entero A8 tiene mejor hit-under-miss en lugar de stalling. El cuello de botella va a ser el ancho de banda de memoria / latencia para los puntos de referencia tan grande en comparación con la caché L1/L2. También es posible que desee ejecutar el benchmark en tamaños más pequeños (4KB..256KB) para ver los efectos cuando los datos se almacenan completamente en caché en L1 y/o L2.

 44
Author: John Ripley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-20 17:28:15

Aunque está limitado por la latencia a la memoria principal en este caso, no es exactamente obvio que la versión NEON sería más lenta que la versión ASM.

Usando la calculadora de ciclos aquí:

Http://pulsar.webshaker.net/ccc/result.php?lng=en

Su código debe tomar 7 ciclos antes de que el cache pierda las penalizaciones. Es más lento de lo esperado porque está utilizando cargas no alineadas y debido a la latencia entre el add y la tienda.

Mientras tanto, el el bucle generado por el compilador tarda 6 ciclos (tampoco está muy bien programado ni optimizado en general). Pero está haciendo una cuarta parte del trabajo.

El ciclo cuenta desde el script puede no ser perfecto, pero no veo nada que se vea descaradamente mal en él, así que creo que al menos estarían cerca. Hay potencial para tomar un ciclo adicional en la rama si maximiza el ancho de banda de recuperación (también si los bucles no están alineados de 64 bits), pero en este caso hay muchos puestos para ocultar que.

La respuesta no es que integer en Cortex-A8 tiene más oportunidades de ocultar la latencia. De hecho, normalmente tiene menos, debido a la tubería escalonada de NEON y la cola de problemas. Por supuesto, esto solo es cierto en Cortex-A8-en Cortex-A9 la situación bien puede invertirse (NEON se envía en orden y en paralelo con integer, mientras que integer tiene capacidades fuera de orden). Desde que etiquetaste este Cortex-A8 asumo que es lo que estás usando.

Esto requiere más investigación. Aqui son algunas ideas por qué esto podría estar sucediendo:

  • No está especificando ningún tipo de alineación en sus matrices, y aunque espero que new se alinee a 8 bytes, puede que no se alinee a 16 bytes. Digamos que realmente está obteniendo matrices que no están alineadas con 16 bytes. Entonces estaría dividiendo entre líneas en el acceso a la caché que podría tener una penalización adicional (especialmente en fallas)
  • Una falta de caché ocurre justo después de una tienda; no creo que Cortex-A8 tenga ninguna desambiguación de memoria y por lo tanto debe asumir que la carga podría ser de la misma línea que la tienda, por lo tanto requiere el búfer de escritura para drenar antes de que la carga faltante L2 puede suceder. Debido a que hay una distancia de tubería mucho mayor entre las cargas de NEÓN (que se inician en la tubería entera) y las tiendas (iniciadas al final de la tubería de NEÓN) que las de enteros, potencialmente habría una pérdida más larga.
  • Debido a que está cargando 16 bytes por acceso en lugar de 4 bytes, el tamaño de la palabra crítica es mayor y por lo tanto la latencia efectiva para un relleno de primera línea de palabras críticas de la memoria principal va a ser mayor (L2 a L1 se supone que está en un bus de 128 bits, por lo que no debería tener el mismo problema)

Preguntaste qué es el NEÓN bueno en casos como este - en realidad, el NEÓN es especialmente bueno para estos casos en los que estás transmitiendo a/desde la memoria. El truco es que necesita usar la precarga para ocultar la latencia de la memoria principal tanto como sea posible. La precarga llevará la memoria a L2 (no a L1) caché antes de tiempo. Aquí NEON tiene una gran ventaja sobre integer porque puede ocultar gran parte de la latencia de caché L2, debido a su canalización escalonada y cola de emisión, pero también porque tiene una ruta directa a ella. Espero que vea una latencia L2 efectiva hasta 0-6 ciclos y menos si tiene menos dependencias y no agota la cola de carga, mientras que en integer puede quedarse atascado con unos buenos ~16 ciclos que no puede evitar (probablemente depende del Cortex-A8).

Así que te recomendaría que alinee sus matrices al tamaño de la línea de caché (64 bytes), desenrolle sus bucles para hacer al menos una línea de caché a la vez, use cargas/almacenes alineados (ponga :128 después de la dirección) y agregue una instrucción pld que cargue varias líneas de caché. En cuanto a cuántas líneas de distancia: comience poco a poco y siga aumentando hasta que ya no vea ningún beneficio.

 17
Author: Exophase,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-30 17:36:49

Su código C++ tampoco está optimizado.

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    unsigned int i = ARR_SIZE_TEST;
    do
    {
        *x++ += *y++;
    } (while --i);
}

Esta versión consume 2 ciclos/iteración menos.

Además, tus resultados de benchmark no me sorprenden en absoluto.

32bit:

Esta función es demasiado simple para NEON. No hay suficientes operaciones aritméticas dejando espacio para optimizaciones.

Sí, es tan simple que tanto la versión de C++ como la de NEON sufren de peligros de tuberías casi siempre sin ninguna posibilidad real de beneficiarse del problema dual capacidad.

Mientras que la versión NEON podría beneficiarse del procesamiento de 4 enteros a la vez, sufre mucho más de cada peligro también. Eso es todo.

8bit:

ARM es MUY lento leyendo cada byte de la memoria. Lo que significa que, mientras que el NEÓN muestra las mismas características que con 32bit, ARM se está quedando fuertemente.

16bit : Lo mismo aquí. Excepto que la lectura de 16 bits de ARM no es TAN mala.

Flotador : La versión C++ se compilará en códigos VFP. Y no hay un VFP completo en Coretex A8, pero VFP lite que no canaliza nada que apeste.

No es que NEON se esté comportando extrañamente procesando 32 bits. Es solo el BRAZO que cumple con la condición ideal. Su función es muy inapropiada para fines de evaluación comparativa debido a su simplicidad. Prueba algo más complejo como la conversión YUV-RGB:

Para su información, mi versión NEON totalmente optimizada funciona aproximadamente 20 veces más rápido que mi versión C totalmente optimizada y 8 veces más rápido que mi conjunto de BRAZO totalmente optimizado versión. Espero que eso te dé alguna idea de lo poderoso que puede ser el NEÓN.

Por último, pero no menos importante, el PLD de instrucción de BRAZO es el mejor amigo de NEON. Colocado correctamente, traerá al menos un 40% de aumento de rendimiento.

 11
Author: Jake 'Alquimista' LEE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-02 13:02:24

Puede intentar alguna modificación para mejorar el código.

Si puedes: - utilice un tercer búfer para almacenar los resultados. - trate de alinear los datos en 8 bytes.

El código debería ser algo como (lo siento, no conozco la sintaxis en línea de gcc)

.loop1:
 vld1.32   {q0}, [%[x]:128]!
 vld1.32   {q1}, [%[y]:128]!
 vadd.i32  q0 ,q0, q1
 vst1.32   {q0}, [%[z]:128]!
 subs     %[i], %[i], $1
bne      .loop1

Como dice Exophase, tienes cierta latencia de canalización. puede ser su puede intentar

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

sub     %[i], %[i], $1

.loop1:
vadd.i32  q2 ,q0, q1

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

vst1.32   {q2}, [%[z]:128]!
subs     %[i], %[i], $1
bne      .loop1

vadd.i32  q2 ,q0, q1
vst1.32   {q2}, [%[z]:128]!

Finalmente, está claro que saturarás el ancho de banda de la memoria

Puedes intentar añadir un pequeño

PLD [%[x], 192]

En tu bucle.

Decir nosotros si es mejor...

 5
Author: webshaker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-07 10:25:50

8ms de diferencia es TAN pequeño que probablemente esté midiendo artefactos de las cachés o tuberías.

EDIT : ¿Intentaste comparar con algo como esto para tipos como float y short, etc.? Esperaría que el compilador lo optimice aún mejor y reduzca la brecha. También en su prueba usted hace la versión de C++ primero entonces la versión de ASM, esto puede tener impacto en el funcionamiento así que escribiría dos programas diferentes para ser más justos.

for ( register int i = 0; i < ARR_SIZE_TEST/4; ++i )
{
    x[ i ] = x[ i ] + y[ i ];
    x[ i+1 ] = x[ i+1 ] + y[ i+1 ];
    x[ i+2 ] = x[ i+2 ] + y[ i+2 ];
    x[ i+3 ] = x[ i+3 ] + y[ i+3 ];
}

Lo último, en la firma de su función, utiliza unsigned* en lugar de unsigned[]. Este último es preferido porque el compilador supone que los arrays no se superponen y se le permite reordenar los accesos. Intente usar la palabra clave restrict también para una mejor protección contra el aliasing.

 2
Author: Giovanni Funchal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-20 20:05:00