STM32F4: Instrucciones de coma flotante demasiado lentas

Estoy trabajando en una aplicación de audio en el Nucleo F411RE y noté que mi procesamiento era demasiado lento, lo que hacía que la aplicación omitiera algunas muestras.

Mirando mi desmontaje, pensé que dada la cantidad de instrucciones y el reloj de CPU de 100 MHz (que configuré en STM32CubeMx), debería ser mucho más rápido.

Revisé el valor de SYSCLK y es 100Mhz como se esperaba. Para estar 100% seguro, puse 1000 "nop" en mi bucle principal y medí 10 µs, lo que corresponde a un reloj de 100 MHz.

Medí exactamente el tiempo que tomó mi procesamiento y tomó 14.5 µs, es decir, 1450 ciclos de reloj. Creo que es demasiado, teniendo en cuenta que el procesamiento es bastante simple:

for(i=0; i<12; i++)
{
    el1.mode[i].phase += el1.osc[i].phaseInc;  // 16 µs
    if(el1.osc[i].phase >= 1.0) // 20 µs (for the whole "if"
        el1.osc[i].phase -= 1.0; 
    el1.osc[i].value = sine[ (int16_t)(el1.osc[i].phase * RES) ]; // 96 µs
    el1.val += el1.osc[i].value * el1.osc[i].amp; // 28 µs
} // that's a total of 1.63 µs for the whole loop

donde fase y faseInc son flotantes de precisión simple y el valor es un int16_t, sine[] es una tabla de búsqueda que contiene 1024 int16_t.

No debería ser más de 500 ciclos, ¿verdad? Miré el desmontaje, usa las instrucciones de punto flotante... Por ejemplo, el desmontaje de la última línea es: vfma.f32 => 3 ciclos vcvt.s32.f32 => 1 ciclo vstr => 2 ciclos ldrh.w = > 2 ciclos

(ciclos de tiempo de acuerdo con esto ) Así que eso es un total de 8 instrucciones para esa línea, que es la "más grande". Realmente no entiendo por qué es tan lento... ¿Tal vez porque estoy usando estructuras o algo así?

Si alguien tiene una idea, me encantaría escucharla.

EDITAR : Acabo de medir el tiempo línea por línea, puedes verlo en el código de arriba. Parece que la línea que consume más tiempo es la línea de la tabla de búsqueda, lo que significaría que es el tiempo de acceso a la memoria lo que es crítico. ¿Cómo podría mejorar eso?

EDIT2: desmontaje, según lo solicitado por BruceAbott (lo siento, es un poco complicado, probablemente debido a la forma en que fue optimizado por el compilador):

membrane1.mode[i].phase += membrane1.mode[i].phaseInc;
0800192e:   vldr s14, [r5, #12]
08001932:   vldr s15, [r5, #8]
08001936:   vadd.f32 s15, s15, s14
0800193a:   adds r5, #24
179 if(membrane1.mode[i].phase >= 1.0)
0800193c:   vcmpe.f32 s15, s16
08001940:   vmrs APSR_nzcv, fpscr
180 membrane1.mode[i].phase -= 1.0;
08001944:   itt ge
08001946:   vmovge.f32 s14, #112    ; 0x70
0800194a:   vsubge.f32 s15, s15, s14
0800194e:   vstr s15, [r5, #-16]
182 membrane1.mode[i].value = sine[(int16_t)(membrane1.mode[i].phase * RES)];
08001952:   ldr.w r0, [r5, #-16]
08001956:   bl 0x80004bc <__extendsfdf2>
0800195a:   ldr r3, [pc, #112]      ; (0x80019cc <main+428>)
0800195c:   movs r2, #0
0800195e:   bl 0x8000564 <__muldf3>
08001962:   bl 0x8000988 <__fixdfsi>
08001966:   ldr r3, [pc, #104]      ; (0x80019d0 <main+432>)
184 membrane1.val += membrane1.mode[i].value * membrane1.mode[i].amp;
08001968:   vldr s13, [r5, #-4]
182 membrane1.mode[i].value = sine[(int16_t)(membrane1.mode[i].phase * RES)];
0800196c:   sxth r0, r0
0800196e:   ldrh.w r3, [r3, r0, lsl #1]
08001972:   strh.w r3, [r5, #-8]
184 membrane1.val += membrane1.mode[i].value * membrane1.mode[i].amp;
08001976:   sxth r3, r3
08001978:   vmov s15, r3
0800197c:   sxth r3, r4
0800197e:   vcvt.f32.s32 s14, s15
08001982:   vmov s15, r3
08001986:   vcvt.f32.s32 s15, s15
174 for(i=0; i<12; i++) // VADD.F32 : 1 cycle
0800198a:   cmp r5, r6
184 membrane1.val += membrane1.mode[i].value * membrane1.mode[i].amp;
0800198c:   vfma.f32 s15, s14, s13
08001990:   vcvt.s32.f32 s15, s15
08001994:   vstr s15, [sp, #4]
08001998:   ldrh.w r4, [sp, #4]
0800199c:   bne.n 0x800192e <main+270>
¿Ha considerado los tiempos de acceso a la memoria?
Bueno, eso no es tan simple de calcular sin(x) 12 veces
Realmente no... Para ser honesto, soy realmente un principiante en la optimización... ¿Pero podría el tiempo de acceso aumentar tanto el tiempo que toma mi procesamiento? ¿Y cómo podría optimizar eso?
@MarkoBuršič Lo siento si mi código no fue claro, sine[] es una tabla de ondas, no una función
¿Está habilitada la FPU (Unidad de punto flotante) del controlador?
Lo es, agregué '-mfloat-abi=hard -mfpu=fpv4-sp-d16 ' a los comandos de mi compilador
@JonathanWheeler Revisé la hoja de datos de la pieza, es una pieza de 'estado de espera 0'. Supongo que está utilizando el método de cambio de pin para comparar su ciclo. Utilice el mismo método para comprobar cada parte. Coloque un alfiler alrededor de la ifdeclaración interna. Si eso es corto, intente colocarlo alrededor de la tabla de búsqueda de senos. Pronto...
@slightlynybbled Muchas gracias, lo intentaré ahora mismo
¿Puede mostrarnos la lista de desmontaje?
@BruceAbbott Editaré mi publicación
Como técnico de audio: por favor , utilice un número entero para la fase. No acumulará jitter y obtendrá el ajuste de forma gratuita si selecciona el tamaño correcto e ignora algunos detalles más finos sobre el estándar C. Esto también convierte todo en operaciones con números enteros, por lo que todo el ciclo no debería tomar más de unos pocos ciclos. Sin embargo, esta no es una respuesta a la pregunta, porque no preguntó cómo optimizar. :)
gracias @pipe, buena idea! ¡Haré eso, vuelvo a medir para ver si aprieta las cosas lo suficiente! Estaba usando flotantes porque al principio estaba haciendo interpolación, pero la interpolación era demasiado lenta, así que tuve que deshacerme de ella.
Bueno, cambié phase y phaseInc a uint16_t pero no cambió el tiempo... Tal vez lo hice de manera incorrecta. Además, la línea de toma de mayor tiempo es membrane1.mode[i].value = sine[membrane1.mode[i].phase];(0.96 µs). ¿Hay alguna manera de optimizar esto?
EDITAR: Logré hacerlo eventualmente, ahora toma solo 5 µs gracias a @pipe... ¡muchas gracias chicos!
acelerar el reloj en un mcu no hace que las cosas funcionen más rápido, el flash a menudo requiere más estados de espera que ralentizan todo. Una vez obtenido, y si el procesador está vinculado, seguro, pero si el flash está vinculado, entonces no. idealmente, desea estar justo debajo del límite de velocidad donde debe cambiar al siguiente estado de espera más grande.
@dwelch No tengo experiencia en el tema, y ​​realmente no entendí tu última oración... ¿Tienes algunos recursos sobre el tema? ¡Gracias!
Creo que escribí una novela en una de estas respuestas SO. y esas cosas en github. Digamos, por ejemplo, que su flash tiene un estado de espera por debajo o a 24 MHz y dos de 24 a 32 y tres de 32 en adelante. entonces, a 24 MHz, puede acceder al flash básicamente a 12 MHz, pero justo por encima de 24 MHz, es de 8 MHz, ya que necesita el siguiente estado de espera. pero a medida que te acercas a mis números de fantasía. a 32 gallos, el flash es de 10,7 MHz, pero justo por encima de los 8 MHz. hasta ahora, 24 es el más rápido en cuanto a acceder al flash (estos son números inventados). el flash se clasificará a una velocidad máxima y ahí es donde se encontrarán esos límites de reloj
los requisitos del estado de espera tendrán límites a velocidades de reloj basadas en el diseño en flash para esa mcu. así que mi punto es su declaración de ir a 100Mhz para hacerlo mucho más rápido, no solo puede hacerlo más rápido sino que hacerlo más lento dependiendo de dónde aterrizan los 100Mhz y si está conectado a flash o al procesador. así que de eso se trataba mi comentario. No leí todas las otras publicaciones, pero la respuesta relacionada con la flotación de 64 bits, usó 1.0 en lugar de 1.0F forzando una flotación suave, que debería haber visto un gran salto en el tamaño del binario, al menos no está ayudando a su actuación.
En caso de que la lentitud se deba (en parte) a los accesos a Flash, puede intentar poner la función en la RAM: stackoverflow.com/questions/15137214/…
@dwelch una pequeña actualización sobre su consejo: investigué los estados de espera para la serie STM32F4 y resulta que es un estado de 0 esperas en cualquier frecuencia de reloj. Sin embargo, me llevas a aprender sobre todo este asunto y lo tendré presente en mis proyectos posteriores. ¡Muchas gracias! Michael: sí, es una buena idea, tu enlace es muy interesante, aunque no se da ningún comentario allí. ¡Le daré el mío tan pronto como ponga esto en la aplicación! Gracias.
Sí, siempre revisa...

Respuestas (1)

En su desmontaje, vemos llamadas a funciones matemáticas de 64 bits (doble precisión): -

08001956:   bl 0x80004bc <__extendsfdf2>
...
0800195e:   bl 0x8000564 <__muldf3>
08001962:   bl 0x8000988 <__fixdfsi>

El STM32F4 solo admite punto flotante de 32 bits en hardware, por lo que estas funciones deben realizarse en software y llevará muchos ciclos ejecutarlas. Para asegurarse de que todos los cálculos se realicen en 32 bits, debe definir todos sus números de coma flotante (incluidas las constantes) como tipo float.

El código OP está escrito para usar el doble, por lo que tiene sentido. comience cambiando el 1.0 a 1.0F, sin que sean el doble según el estándar del idioma y las cosas a su alrededor deben promoverse.
Intenté cambiar mi constante RES a 1024.0f y me ayudó mucho. ¡Muchas gracias a ustedes dos! No tenía idea de que el estándar del lenguaje hacía que todas las constantes flotantes fueran dobles y promovía todo... ¿Hay alguna manera de cambiar ese comportamiento con las opciones del compilador, por ejemplo?
eso sería específico del compilador, tal vez, tal vez no. pero dentro del idioma solo use la F para especificar un solo flotante. o tal vez typecast (float)(1.0), el compilador debería optimizar la conversión y hacer que se compile en tiempo, no en tiempo de ejecución.