Mi prueba de velocidad "flash VS RAM" no funciona. ¿Por qué?

Al tratar de demostrar que copiar información de una cadena en la memoria flash a la RAM lleva más tiempo que copiar la misma información en una matriz en la RAM a otra, ejecuté el siguiente código en el PIC32 USB Starter Kit II (PIC32MX795F512L):

// Global variables:
char b[60000] = "Initialized";
char c[] = "Hello";

// In main():
// Version 1: Copying from flash to RAM:
    while(1){
        strcpy(b, "Hello");
         for (i = 0; i < 999; i++){
             strcat(b, "Hello");  
         }
         PORTD ^= 1; // Toggle LED

    }

// Version 2: Copying from RAM to RAM:
    while(1){
        strcpy(b, c);
         for (i = 0; i < 999; i++){
             strcat(b, c);  
         }
         PORTD ^= 1; // Toggle LED

    }

Esperaba ver el LED parpadear más rápido en la versión 2, ¡pero en cambio la versión 1 fue mucho más rápida! ¿Cómo podría ser esto?

¿Podría ser que en lugar de copiar información de flash, estemos usando datos inmediatos codificados en lenguaje de máquina MIPS? Tal vez debería tratar de entender el código MIPS.

¡Gracias de antemano!

Respuestas (3)

Hay varias explicaciones posibles muy diferentes. Aquí hay dos que pensé:

  1. el reloj de la CPU funciona tan lentamente que FLASH es tan rápido como la RAM, y debido a que Flash to RAM usa dos buses independientes, en realidad tiene más ancho de banda.

EDITAR: Miré una hoja de datos de Microchip para PIC32MX5XX / 6XX / 7XX
En la Tabla 31-12: "PROGRAMA FLASH MEMORY WAIT STATE CHARACTERISTIC", dice:

  • 0 Estado de espera 0 a 30 MHz
  • 1 Estado de espera 31 a 60 MHz
  • 2 Estados de espera 61 a 80 MHz

Entonces, si el reloj de la CPU es de 30 MHz o menos, la memoria Flash puede mantenerse al día sin esperas. No puedo encontrar ninguna especificación de tiempo para la SRAM, así que asumo que no tiene estados de espera a ninguna velocidad. Por lo tanto, ejecutar Flash a 30 MHz o menos debería ser tan rápido como SRAM.

Incluso por encima de esa velocidad de reloj de 30 MHz, los estados de espera de Flash pueden tener un impacto mucho menor de lo esperado debido a la 'caché de captación previa'. Este caché tiene 16 líneas de caché de 16 bytes. Por lo tanto, si el ciclo del programa tiene menos de 256 bytes (lo que es factible para ese ciclo), una vez que se carga la caché de captación previa, todo el programa puede ejecutarse desde la caché de captación previa, sin ningún acceso adicional a Flash. Claramente, esto beneficia a ambos bucles. Sin embargo, la versión 2 accede a la RAM dos veces para leer y escribir datos, mientras que la versión 1 solo puede acceder a la memoria para escribir en la RAM.

Permitiendo la explicación 2, también, si el compilador también ha 'optimizado' hacer que strcat(b, "Hello");el ciclo de la versión 1 sea más rápido que el ciclo de la versión 2, entonces el único acceso en la versión 1 a la memoria es almacenar bytes en b. Eso debería ser significativamente más rápido que copiar de RAM a RAM.

  1. El compilador optimiza la Versión 1 como loco. "Hola" es una constante. Incluso podría caber en dos registros de 32 bits, por lo que el compilador podría convertir la versión 1 en un bucle muy cerrado. Supongo que los relojes son correctos, y alguna versión de esto es la explicación.

La optimización de la versión 1 está especialmente bien hecha para los compiladores que tienen un conocimiento adecuado de strcpy y strcat. gcc tiene versiones integradas internas de strcpy y strcat , que puede elegir usar en las circunstancias apropiadas. También gcc optimiza strcpy y strcat para varios procesadores en diferentes secuencias), creo que incluso podría expandir el strcat en línea en algunos casos, pero no puedo encontrar la referencia.

Así que descargue el ensamblador y eche un vistazo. Para gcc ARM Cortex-M es arm-none-eabi-objdump. Esto descargará una versión textual de su programa, mostrando el ensamblador, generalmente organizado en funciones y, si se usan las opciones correctas, puede entremezclar la fuente C original como comentarios, haciendo que sea relativamente fácil encontrar las instrucciones del ensamblador que corresponden a tu codigo. (Aunque tenga en cuenta que, debido a la optimización, este mapeo podría no ser perfecto)

Si los datos, "Hola" en la versión 1, simplemente se cargan en los registros y se escriben en la RAM en un ciclo cerrado, entonces puede quedar claro en un volcado del ensamblador, incluso sin un conocimiento profundo del ensamblador MIPS.

¿Qué pasa si quieres hacer una comparación real de flash vs RAM?
Podría dificultar la optimización para el compilador e intentar evitar que optimice las dos versiones del ciclo de manera diferente.

Un enfoque sería obligar al compilador a almacenar el "Hola" en una variable que usted fuerza en Flash.

No conozco el mecanismo para MIPS, pero es muy probable que haya un pragma o una forma de solicitar que el enlazador coloque una variable en el segmento flash del programa.

Para gcc para ARM, una variable se puede 'decorar' con una anotación:
const uint8_t array[10] __attribute__((section(".eeprom"), used))
esto marca la variable para que se coloque en la sección ".eeprom" del enlazador, y el script de enlace del enlazador garantiza que todas las direcciones para esa sección estén en la memoria Flash. rango de direcciones).

Sin embargo, es posible que también deba anular las optimizaciones del compilador cuando se aplican a un valor de cadena constante.

Coloque una versión de uso general del ciclo while en una función separada, myfunc(a *char, b *char). Luego llámelo con dos conjuntos diferentes de variables (RAM a RAM vs FLASH a RAM). Normalmente, esto debería obligar al compilador a generar un conjunto de código (el cuerpo de myfunc), que se utilizará para ambos casos. Eso daría una comparación de 'manzanas por manzanas'. Sin embargo, no subestime la capacidad de optimización de los compiladores. Es posible que aún desee volcar el ensamblador para verificar que el compilador no sea demasiado inteligente.

(Pondría un límite en el número de iteraciones para evitar que escriba en los periféricos)

Todo esto es especulación. Deberá proporcionar más información, específicamente la inicialización del reloj de la CPU, los buses y los búferes, el compilador que está utilizando e, idealmente, un volcado del ensamblador, para dar respuestas más precisas.

Sin embargo, hacer el cambio a una sola función que ejecuta un bucle for grande y llamarlo con dos conjuntos diferentes de parámetros podría ser suficiente para satisfacer su requisito.

Estoy impresionado con su amabilidad y su profundidad de conocimiento. ¡Muchas gracias! Veré qué puedo hacer con el código de lenguaje ensamblador. El problema que tengo con ese enfoque es que el IDE que estoy usando, MPLAB X IDE, me muestra el código desensamblado sin etiquetas retenidas. Prefiero que el compilador genere el código ensamblador directamente, con las etiquetas retenidas. ¿Alguien sabe cómo hacer eso?
@VititKantabutra: me disculpo, pero no sé cómo hacer que MPLAB X IDE descargue el ensamblador 'útil'. Estos enlaces pueden ayudar microchip.com/forums/m537589.aspx stackoverflow.com/questions/24914860/…

Su propio análisis es correcto.

Lo más probable es que la función strcpy(&, "") para cadenas pequeñas se optimice en seis comandos LoadImmediate posteriores, a menos que la optimización esté completamente desactivada , lo que establece un valor específico en un byte (registro o de otro modo, si está disponible). No optimizará más que eso, porque el tipo de fuente es char, que son bytes. Pero 6 LDI sigue siendo más rápido que 6*2*LD. O si admite RAM directa a RAM, podría funcionar en 6 * LD, pero como la opción 2 es más lenta, probablemente no.

Si desea que la cadena provenga del espacio de datos Flash, la única garantía es averiguar cómo Microchip define una matriz Flash, al igual que sus otras variables globales, puede decirle al sistema que debe crear una matriz de variables en Flash.

Cualquiera que sea la forma de configurarlo dependerá de su entorno, debe buscar "Store Array in Flash PIC" seguido del nombre de su compilador o entorno.

EDITAR:

Para evitar comentarios interminables: sí, las cadenas largas codificadas pueden convertirse en matrices flash (en algunas configuraciones de optimización), pero no existen reglas estrictas sobre lo que se considera "largo", universalmente. Así que mantengo mi declaración de "la única garantía es..."

  • Deshabilitar optimizaciones.
  • Declare todas las matrices como volátiles.

Hecho.

Miré una hoja de datos de Microchip para el PIC32MX5XX/6XX/7XX En la Tabla 31-12: "PROGRAMA DE MEMORIA FLASH ESTADO DE ESPERA CARACTERÍSTICA", dice: - 0 Estado de espera 0 a 30 MHz - 1 Estado de espera 31 a 60 MHz - 2 estados de espera de 61 a 80 MHz Por lo tanto, son estados de espera de Flash. En mi experiencia, las CPU con 'velocidades de instrucción' superiores a 40 MHz (ignore las CPU de múltiples relojes/instrucciones) tienen estados de espera de memoria Flash. IIRC, uno de los fabricantes japoneses de MCU (¿Toshiba, Fujitsu?) tiene Flash mucho más rápido que el MCU m/f promedio.
@gbulmer Ah, me perdí esa parte, gracias. Entonces, de hecho, puede haber un retraso para esta parte, dependiendo del reloj de MCU. Sin embargo, creo que el método de evaluación comparativa actual del OP sería demasiado crudo para detectar esos estados de espera.
Sí, el manual es un poco incómodo porque la sección de memoria Flash solo menciona la programación flash, y el comportamiento del flash es una tabla en una enorme sección de "características eléctricas". He sugerido una forma de anular algunas de las optimizaciones de los compiladores y garantizar que la mayor parte del bucle sea el mismo código. Sin embargo, incluso eso podría mostrar que "Flash to RAM" es más rápido que "RAM to RAM" según la secuencia de código MIPS real generada. En mi humilde opinión, su valor es reescribir para ejecutar el mismo código de bucle para cada caso, pero veamos el ensamblador; de lo contrario, como los banqueros, podríamos especular desastrosamente.
@gbulmer El código de evaluación comparativa confiable se vería algo así como for(;;) PORT ^= variable;donde se declara "variable" volatiley se ubica en RAM o ROM. El código de sobrecarga (el bucle y el XOR) es mínimo y de longitud estática, por lo que puede contar fácilmente la cantidad de ciclos de CPU causados ​​por la sobrecarga, al desmontarlos.
AFAICT, el OP está interesado en la velocidad relativa de Flash y RAM. No he comprobado el PIX32MX7xx, pero los buses periféricos suelen ser mucho más lentos que la RAM y suelen tener muy poco almacenamiento en búfer (es decir, un único búfer de escritura). Entonces, en mi humilde opinión, for(;;) PORT ^= variable;puede ser inútil. Puede causar un cuello de botella en el acceso al bus periférico y decirle al OP muy poco sobre RAM vs Flash. Además, la 'Caché de captación previa' puede ser invisible, por lo que, incluso si el compilador genera instrucciones de carga para la variable residente de Flash, el valor se proporciona desde la caché y es tan rápido como la RAM. El ensamblador podría no decirnos lo suficiente
@gbulmer Ach, maldita sea, caché de datos :) Sí, entonces obviamente no funcionará. Entonces tendrías que escribir algún truco semi-avanzado para superar las 16 líneas de caché, o deshabilitar el caché. Parece que esto sería posible a través de escrituras de registro en el registro de control de caché CHECON.
El caso de Prefetch es para Flash, por lo que podría acelerar el acceso a una variable residente en Flash y al código, pero no afectar (con suerte) el acceso a un periférico o RAM. Sí, la caché Prefetch se puede desactivar. Sin embargo, eso podría distorsionar los resultados de referencia de una manera muy diferente y poco representativa. Hasta que obtengamos más información del OP, dejaré de preocuparme por eso. La evaluación comparativa es difícil, incluso cuando recordamos que 'La evaluación comparativa es difícil' :-)