una recorrer programar matriz matrices llenar for extraer elementos datos columna ciclo almacenar agregar android c android-ndk arm neon

android - recorrer - ¿Cómo puedo optimizar una multiplicación de vector de matriz 4D en bucle con ARM NEON?



programar matrices en matlab (3)

Estoy trabajando en la optimización de una multiplicación vectorial de matriz 4D (128 bits) utilizando ARM NEON Assembler.

Si cargué la matriz y el vector en los registros NEON y los transformo, no obtendré un gran aumento de rendimiento, ya que el cambio a los registros NEON cuesta 20 ciclos. Además recargo la matriz para cada multiplicación, a pesar de que no ha cambiado.

Hay suficiente espacio de registro para realizar la transformación en más vectores por vez. Esto ES un rendimiento cada vez mayor.

Pero..

Me pregunto qué tan rápido sería esta operación si hago el ciclo sobre todos los vértices (aumentando los punteros) dentro del ensamblador. Pero estoy en el comienzo de ensamblador de neón y aunque no sé cómo hacer esto. ¿Puede alguien ayudarme con eso?

Lo que quiero lograr:

  1. matriz de carga y primer vector
  2. cuenta de bucle de tienda "cuenta" y ..
  3. - LOOP_START -
  4. realizar multiplicaciones-adiciones (hacer la Transformación)
  5. escribe q0 en vOut
  6. Aumenta los punteros vIn y vOut en 4 (128 Bit)
  7. CARGAR vIn a q5.
  8. - LOOP_END -

Versión C existente del ciclo:

void TransformVertices(ESMatrix* m, GLfloat* vertices, GLfloat* normals, int count) { GLfloat* pVertex = vertices; int i; // iterate trough vertices only one at a time for (i = 0; i < count ; i ++) { Matrix4Vector4Mul( (float *)m, (float *)pVertex, (float *)pVertex); pVertex += 4; } //LoadMatrix( (const float*) m); //// two at a time //for (i = 0; i < count ; i += 2) //{ // Matrix4Vector4Mul2( (float *)m, (float *)pVertex, (float *)(pVertex + 4)); // pVertex += 8; //} }

Siguiendo el código de NEON-Version al hacer solo una transformación:

void Matrix4Vector4Mul (const float* m, const float* vIn, float* vOut) { asm volatile ( "vldmia %1, {q1-q4 } /n/t" "vldmia %2, {q5} /n/t" "vmul.f32 q0, q1, d10[0] /n/t" "vmla.f32 q0, q2, d10[1] /n/t" "vmla.f32 q0, q3, d11[0] /n/t" "vmla.f32 q0, q4, d11[1] /n/t" "vstmia %0, {q0}" : // no output : "r" (vOut), "r" (m), "r" (vIn) : "memory", "q0", "q1", "q2", "q3", "q4", "q5" ); }

C-Versión de transformación:

void Matrix4Vector4Mul (const float* m, const float* vIn, float* vOut) { Vertex4D* v1 = (Vertex4D*)vIn; Vertex4D vOut1; Vertex4D* l0; Vertex4D* l1; Vertex4D* l2; Vertex4D* l3; // 4x4 Matrix with members m00 - m33 ESMatrix* m1 = (ESMatrix*)m; l0 = (Vertex4D*)&m1->m00; vOut1.x = l0->x * v1->x; vOut1.y = l0->y * v1->x; vOut1.z = l0->z * v1->x; vOut1.w = l0->w * v1->x; l1 = (Vertex4D*)&m1->m10; vOut1.x += l1->x * v1->y; vOut1.y += l1->y * v1->y; vOut1.z += l1->z * v1->y; vOut1.w += l1->w * v1->y; l2 = (Vertex4D*)&m1->m20; vOut1.x += l2->x * v1->z; vOut1.y += l2->y * v1->z; vOut1.z += l2->z * v1->z; vOut1.w += l2->w * v1->z; l3 = (Vertex4D*)&m1->m30; vOut1.x += l3->x * v1->w; vOut1.y += l3->y * v1->w; vOut1.z += l3->z * v1->w; vOut1.w += l3->w * v1->w; *(vOut) = vOut1.x; *(vOut + 1) = vOut1.y; *(vOut + 2) = vOut1.z; *(vOut + 3) = vOut1.w; }

Rendimiento: (Transformar> 90 000 vértices | Android 4.0.4 SGS II)

C-Version: 190 FPS NEON-Version: 162 FPS ( .. slower -.- ) --- LOAD Matrix only ONCE (seperate ASM) and then perform two V''s at a time --- NEON-Version: 217 FPS ( + 33 % NEON | + 14 % C-Code )


¿Intentó jugar con las banderas del compilador?

-mcpu=cortex-a9 -mtune=cortex-a9 -mfloat-abi=softfp -mfpu=neon -O3

funciona bastante bien para mí en este caso (gcc 4.4.3, distribuido con Android NDK 8b). Intenta tener un código fuente estricto definiendo funciones internas estáticas y en línea, así como moviendo la matriz (m [X] [0] cosas) a variables globales estáticas o simplemente combina Matrix4Vector4Mul en bucle y crea variables locales de matriz en lugar de seguir pasándolo en función - gcc no se pone inteligente allí.

Cuando hago esto, me pongo a continuación para el ciclo principal.

a4: ed567a03 vldr s15, [r6, #-12] a8: ee276aa0 vmul.f32 s12, s15, s1 ac: ee676aa8 vmul.f32 s13, s15, s17 b0: ed564a04 vldr s9, [r6, #-16] b4: ee277a88 vmul.f32 s14, s15, s16 b8: ed165a02 vldr s10, [r6, #-8] bc: ee677a80 vmul.f32 s15, s15, s0 c0: ed565a01 vldr s11, [r6, #-4] c4: e2833001 add r3, r3, #1 c8: ee046a89 vmla.f32 s12, s9, s18 cc: e1530004 cmp r3, r4 d0: ee446aaa vmla.f32 s13, s9, s21 d4: ee047a8a vmla.f32 s14, s9, s20 d8: ee447aa9 vmla.f32 s15, s9, s19 dc: ee056a22 vmla.f32 s12, s10, s5 e0: ee456a01 vmla.f32 s13, s10, s2 e4: ee057a21 vmla.f32 s14, s10, s3 e8: ee457a02 vmla.f32 s15, s10, s4 ec: ee056a8b vmla.f32 s12, s11, s22 f0: ee456a83 vmla.f32 s13, s11, s6 f4: ee057aa3 vmla.f32 s14, s11, s7 f8: ee457a84 vmla.f32 s15, s11, s8 fc: ed066a01 vstr s12, [r6, #-4] 100: ed466a04 vstr s13, [r6, #-16] 104: ed067a03 vstr s14, [r6, #-12] 108: ed467a02 vstr s15, [r6, #-8] 10c: e2866010 add r6, r6, #16 110: 1affffe3 bne a4 <TransformVertices+0xa4>

Tener 4 cargas, 4 multiplicaciones, 12 multiplicaciones y acumulaciones y 4 tiendas que coinciden con lo que estás haciendo en Matrix4Vector4Mul.

Si todavía no está satisfecho con el código generado por el compilador, pase el compilador ''-S'' para obtener el resultado del ensamblado y utilícelo como punto de partida para mejorar aún más en lugar de comenzar desde cero.

También debe verificar que los vertices estén alineados con el tamaño de la línea de caché (32 bytes para Cortex-A9) para obtener un buen flujo de datos.

Para la vectorización hay opciones de gcc como -ftree-vectorizer-verbose=9 para imprimir información de lo que se vectorizó. También busque en la documentación de gcc esta para ver cómo puede dirigir gcc o qué necesita modificar para obtener sus multiplicaciones vectorizadas. Esto puede parecer mucho para excavar, pero sería más fructífero para ti en el largo plazo que ''vectorizar a mano''.


La versión de neón sintonizada a mano sufre de dependencia entre todas las operaciones, mientras que gcc puede realizar una programación fuera de servicio para la versión c. Debería poder mejorar la versión NEON calculando paralelamente dos o más hilos independientes:

El incremento del puntero (incremento posterior) en NEON se hace con un signo de exclamación. Esos registros se deben incluir en la lista de registros de salida "= r" (vOut)

vld1.32 {d0,d1}, [%2]! ; // next round %2=%2 + 16 vst1.32 {d0}, [%3]! ; // next round %3=%3 + 8

Otro modo de direccionamiento permite el incremento posterior por un "paso" definido en otro registro de brazo. La opción está disponible solo en algunos comandos de carga (ya que hay una variedad de opciones de intercalado así como la carga de elementos elegidos de say d1 [1] (parte superior)).

vld1.16 d0, [%2], %3 ; // increment by register %3

El incremento del contador ocurre con la secuencia

1: subs %3, %3, #1 ; // with "=r" (count) as fourth argument bne 1b ; // create a local label

Se utiliza la etiqueta local, ya que dos instrucciones "bne loop" en el mismo archivo provocan un error

Uno debería ser capaz de aumentar el paralelismo por un factor de cuatro calculando las adiciones multiplicadas fusionadas para los vectores en lugar de los elementos individuales.

En este caso, vale la pena realizar una transposición de matriz por adelantado (antes de llamar a la rutina o con el modo de direccionamiento especial).

asm( "vld1.32 {d0[0],d2[0],d4[0],d6[0]}, [%0]! /n/t" "vld1.32 {d0[1],d2[1],d4[1],d6[1]}, [%0]! /n/t" "vld1.32 {d1[0],d3[0],d5[0],d7[0]}, [%0]! /n/t" "vld1.32 {d1[1],d3[1],d5[1],d7[1]}, [%0]! /n/t" "vld1.32 {q8}, [%2:128]! /n/t" "vld1.32 {q9}, [%2:128]! /n/t" "vld1.32 {q10}, [%2:128]! /n/t" "vld1.32 {q11}, [%2:128]! /n/t" "subs %0, %0, %0 /n/t" // set zero flag "1: /n/t" "vst1.32 {q4}, [%1:128]! /n/t" "vmul.f32 q4, q8, q0 /n/t" "vst1.32 {q5}, [%1:128]! /n/t" "vmul.f32 q5, q9, q0 /n/t" "vst1.32 {q6}, [%1:128]! /n/t" "vmul.f32 q6, q10, q0 /n/t" "vst1.32 {q7}, [%1:128]! /n/t" "vmul.f32 q7, q11, q0 /n/t" "subne %1,%1, #64 /n/t" // revert writing pointer in 1st iteration "vmla.f32 q4, q8, q1 /n/t" "vmla.f32 q5, q9, q1 /n/t" "vmla.f32 q6, q10, q1 /n/t" "vmla.f32 q7, q11, q1 /n/t" "subs %2, %2, #1 /n/t" "vmla.f32 q4, q8, q2 /n/t" "vmla.f32 q5, q9, q2 /n/t" "vmla.f32 q6, q10, q2 /n/t" "vmla.f32 q7, q11, q2 /n/t" "vmla.f32 q4, q8, q3 /n/t" "vld1.32 {q8}, [%2:128]! /n/t" // start loading vectors immediately "vmla.f32 q5, q9, q3 /n/t" "vld1.32 {q9}, [%2:128]! /n/t" // when all arithmetic is done "vmla.f32 q6, q10, q3 /n/t" "vld1.32 {q10}, [%2:128]! /n/t" "vmla.f32 q7, q11, q3 /n/t" "vld1.32 {q11}, [%2:128]! /n/t" "jnz b1 /n/t" "vst1.32 {q4,q5}, [%1:128]! /n/t" // write after first loop "vst1.32 {q6,q7}, [%1:128]! /n/t" : "=r" (m), "=r" (vOut), "=r" (vIn), "=r" ( N ), : : "d0","d1","q0", ... ); // marking q0 isn''t enough for some gcc version

Lee y escribe en bloques alineados de 128 bits (asegúrate de que la ptr de datos también esté alineada)
hay un malloc con alinear, o simplemente ajustar manualmente ptr=((int)ptr + 15) & ~15 .

Del mismo modo que hay un bloque post loop que escribe los resultados, uno puede escribir un bloque de pre loop similar que omita la primera escritura de tonterías en vOut (que también podría superarse con la escritura condicional). Desafortunadamente, uno solo puede escribir registros de 64 bits de forma condicional.


Ya es casi un tema de todo un año, pero creo que es importante darle la respuesta "correcta", ya que aquí hay algo muy sospechoso, y nadie lo ha señalado hasta ahora:

  1. Debe evitar usar q4-q7 si es posible, ya que deben conservarse antes de su uso

  2. Corrígeme si me equivoco con esto, pero si mi memoria no me falla, solo d0 ~ d3 (o d0 ~ d7) puede contener escalares. Realmente me pregunto por qué gcc tolera d10 y d11 como operandos escalares. Ya que es físicamente imposible de esa manera, supongo que gcc está haciendo algo loco con su ensamblaje en línea. Verifique el desmontaje de su código de montaje en línea.

Es cierto que su código ensamblador en línea sufre de dos enclavamientos (2 ciclos después de la carga y 9 ciclos antes de la tienda), pero es inimaginable para mí que el código NEON corra más lento que el código C.

Es una gran conjetura de mi parte que gcc hace un pesado registro de transferencia hacia adelante y hacia atrás en lugar de escupir un mensaje de error. Y no está haciendo exactamente un favor en este caso.