c++ arm neon cortex-a8

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



cortex-a8 (5)

8ms de diferencia es tan pequeño que probablemente esté midiendo los artefactos de las caches o tuberías.

EDITAR : ¿Intentó comparar con algo como esto para tipos como float y short, etc.? Espero que el compilador lo optimice aún mejor y reduzca la brecha. También en tu prueba, primero haces la versión C ++ y luego la versión ASM, esto puede tener un impacto en el rendimiento, 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, usa unsigned* lugar de unsigned[] . Se prefiere este último porque el compilador supone que las matrices 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 alias.

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é el neón es más lento con tipos enteros de 32 bits?

Utilicé la última versión de GCC para Android NDK. Las banderas de optimización NEON estaban activadas. Aquí hay una versión de C ++ desmontada:

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 de neón desmontada:

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é el neón es más lento con tipos enteros de 32 bits?


La tubería NEON en Cortex-A8 está en ejecución y está limitada a fallos (sin cambio de nombre), por lo que está limitado por la latencia de la memoria (ya que usa más del 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 la memoria. Esto explicaría por qué el código NEON es un poco más lento (no mucho) que el que no es NEON.

Debe desenrollar los bucles de ensamblaje 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, así que puedes desenrollarlo mucho. El código de enteros sufrirá el mismo problema, en menor medida porque el entero A8 tiene mejores resultados en lugar de estancamiento. El cuello de botella será el ancho de banda / latencia de la memoria para los puntos de referencia tan grandes en comparación con el caché L1 / L2. También es posible que desee ejecutar el punto de referencia en tamaños más pequeños (4KB..256KB) para ver los efectos cuando los datos se almacenan en caché completamente en L1 y / o L2.


Puedes probar alguna modificación para mejorar el código.

Si puede: - usar un tercer búfer para almacenar resultados. - Intenta alinear los datos en 8 bytes.

El código debería ser algo como (lo siento, no sé 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 un poco de latencia de tubería. puede ser tu intento

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á el ancho de banda de la memoria

Puedes intentar añadir una pequeña

PLD [%[x], 192]

en su bucle.

dinos si es mejor ...


Si bien la latencia de la memoria principal lo limita en este caso, no es exactamente obvio que la versión NEON sea 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 que puede esperar porque está usando cargas no alineadas y debido a la latencia entre el complemento y la tienda.

Mientras tanto, el bucle generado por el compilador toma 6 ciclos (tampoco está muy bien programado u optimizado en general). Pero está haciendo un cuarto de trabajo tanto.

Es posible que el conteo de ciclos del guión no sea perfecto, pero no veo nada que parezca claramente incorrecto, por lo que creo que al menos estarían cerca. Existe la posibilidad de realizar un ciclo adicional en la rama si alcanza el ancho de banda máximo (también si los bucles no están alineados en 64 bits), pero en este caso hay muchos puestos para ocultarlo.

La respuesta no es que el número entero en Cortex-A8 tenga más oportunidades para 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 puede revertirse (NEON se despacha en orden y en paralelo con un entero, mientras que el entero tiene capacidades fuera de orden). Desde que etiquetaste este Cortex-A8, asumo que eso es lo que estás usando.

Esto exige más investigación. Aquí hay algunas ideas de por qué esto podría estar sucediendo:

  • No está especificando ningún tipo de alineación en sus arreglos, y aunque espero que la nueva alineación sea de 8 bytes, podría no estar alineada 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 al caché, lo que podría tener una penalización adicional (especialmente en fallas)
  • Un error de caché ocurre justo después de una tienda; No creo que Cortex-A8 tenga ninguna desambiguación de memoria y, por lo tanto, debo suponer que la carga podría ser de la misma línea que el almacén, por lo que es necesario que el búfer de escritura se agote antes de que pueda ocurrir la carga faltante de L2. Debido a que hay una distancia de tubería mucho mayor entre las cargas de NEON (que se inician en la tubería de enteros) y las tiendas (que se inician al final de la tubería de NEON) que las enteras, podría haber un estancamiento más largo.
  • 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 primer relleno de línea desde la memoria principal va a ser mayor (se supone que L2 a L1 estar en un bus de 128 bits por lo que no debería tener el mismo problema)

Preguntó qué tan bueno es NEON en casos como este; en realidad, NEON es especialmente bueno para estos casos en los que está 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 obtendrá memoria en el caché L2 (no L1) antes de tiempo. Aquí, NEON tiene una gran ventaja sobre los enteros porque puede ocultar gran parte de la latencia de caché L2, debido a su canalización escalonada y a la cola de emisión, pero también porque tiene una ruta directa. Espero que vea una latencia L2 efectiva de 0 a 6 ciclos y menos si tiene menos dependencias y no agota la cola de carga, mientras que en el número entero puede quedarse con unos buenos ~ 16 ciclos que no puede evitar (probablemente Depende de la Cortex-A8 aunque).

Por lo tanto, le recomiendo que alinee sus matrices con el 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 / tiendas alineadas (ponga: 128 después de la dirección) y agregue una Instrucción pld que carga varias líneas de caché de distancia. En cuanto a cuántas líneas de distancia: comienza pequeño y sigue aumentando hasta que ya no veas ningún beneficio.


Tu 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 menos / iteración.

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

32 bits:

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

Sí, es tan simple que tanto la versión de C ++ como la de NEON sufren de peligros en la tubería casi todas las veces sin ninguna posibilidad real de beneficiarse de las capacidades de doble problema.

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

8 bits :

ARM es muy lento leyendo cada byte de la memoria. Lo que significa que, si bien NEON muestra las mismas características que con 32 bits, ARM está muy rezagado.

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

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

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

Para mi información, mi versión NEON completamente optimizada se ejecuta aproximadamente 20 veces más rápido que mi versión C totalmente optimizada y 8 veces más rápida que mi versión de ensamblaje ARM totalmente optimizada. Espero que te den una idea de lo poderoso que puede ser NEON.

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