Mein Geschwindigkeitstest "Flash VS RAM" funktioniert nicht. Warum?

Um zu demonstrieren, dass das Kopieren von Informationen von einer Zeichenfolge im Flash-Speicher in den RAM länger dauert als das Kopieren derselben Informationen in einem Array im RAM in ein anderes, habe ich den folgenden Code auf dem PIC32 USB Starter Kit II (PIC32MX795F512L) ausgeführt:

// 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

    }

Ich hatte erwartet, dass die LED in Version 2 schneller blinkt, aber stattdessen war Version 1 viel schneller! Wie konnte das sein?

Könnte es sein, dass wir, anstatt Informationen aus dem Flash zu kopieren, unmittelbare Daten verwenden, die in MIPS-Maschinensprache fest codiert sind? Vielleicht sollte ich versuchen, den MIPS-Code zu verstehen.

Vielen Dank im Voraus!

Antworten (3)

Es gibt mehrere sehr unterschiedliche mögliche Erklärungen. Da sind mir zwei eingefallen:

  1. Die CPU-Uhr läuft so langsam, dass FLASH so schnell ist wie RAM, und da Flash to RAM zwei unabhängige Busse verwendet, hat es tatsächlich mehr Bandbreite.

BEARBEITEN: Ich habe mir ein Microchip-Datenblatt für den PIC32MX5XX/6XX/7XX
angesehen . In Tabelle 31-12: "PROGRAM FLASH MEMORY WAIT STATE CHARACTERISTIC" heißt es:

  • 0 Wartezustand 0 bis 30 MHz
  • 1 Wartezustand 31 bis 60 MHz
  • 2 Wartezustände 61 bis 80 MHz

Wenn also der CPU-Takt 30 MHz oder weniger beträgt, kann der Flash-Speicher ohne Wartezeiten mithalten. Ich kann keine Timing-Spezifikation für das SRAM finden, daher gehe ich davon aus, dass es bei keiner Geschwindigkeit Wartezustände gibt. Daher sollte Flash mit 30 MHz oder weniger so schnell sein wie SRAM.

Selbst oberhalb dieser 30-MHz-Taktrate könnten die Flash-Wartezustände aufgrund des „Prefetch-Cache“ einen viel geringeren Einfluss haben als erwartet. Dieser Cache hat 16 16-Byte-Cache-Zeilen. Wenn also die Programmschleife weniger als 256 Bytes groß ist (was für diese Schleife machbar ist), kann das gesamte Programm, sobald der Prefetch-Cache geladen ist, ohne weiteren Zugriff auf Flash aus dem Prefetch-Cache ausgeführt werden. Dies kommt eindeutig beiden Schleifen zugute. Version 2 greift jedoch zweimal auf RAM zu, um Daten zu lesen und zu schreiben, während Version 1 möglicherweise nur auf Speicher zugreift, um in RAM zu schreiben.

Unter Berücksichtigung von Erklärung 2, wenn der Compiler auch "optimiert" hat, strcat(b, "Hello");die Version 1-Schleife schneller als die Version 2-Schleife zu machen, dann besteht der einzige Zugriff in Version 1 auf den Speicher darin, Bytes in zu speichern b. Das sollte deutlich schneller sein, als von RAM zu RAM zu kopieren.

  1. Der Compiler optimiert Version 1 wie verrückt. "Hallo" ist eine Konstante. Es könnte sogar in zwei 32-Bit-Register passen, sodass der Compiler Version 1 in eine sehr enge Schleife verwandeln könnte . Ich gehe davon aus, dass die Uhren korrekt sind, und eine Version davon ist die Erklärung.

Die Optimierung von Version 1 ist besonders gut für Compiler geeignet, die über ausreichende Kenntnisse von strcpy und strcat verfügen. gcc hat intern eingebaute Versionen von strcpy und strcat , die es unter geeigneten Umständen verwenden kann. Außerdem optimiert gcc strcpy und strcat für mehrere Prozessoren in verschiedene Sequenzen), ich glaube, es könnte in einigen Fällen sogar strcat inline erweitern, kann aber die Referenz nicht finden.

Also den Assembler wegwerfen und nachsehen. Für gcc ARM Cortex-M ist es arm-none-eabi-objdump. Dadurch wird eine Textversion Ihres Programms ausgegeben, die den Assembler zeigt, normalerweise in Funktionen organisiert, und wenn die richtigen Optionen verwendet werden, kann es die ursprüngliche C-Quelle als Kommentare vermischen, wodurch es relativ einfach ist, die entsprechenden Assembler-Anweisungen zu finden dein Code. (Beachten Sie jedoch, dass diese Zuordnung aufgrund von Optimierungen möglicherweise nicht perfekt ist.)

Wenn die Daten "Hello" in Version 1 nur in Register geladen und in einer engen Schleife in den RAM geschrieben werden, kann dies aus einem Assembler-Dump auch ohne tiefgreifende Kenntnisse des MIPS-Assemblers ersichtlich sein.

Was ist, wenn Sie einen tatsächlichen Vergleich zwischen Flash und RAM durchführen möchten?
Sie könnten die Optimierung für den Compiler erschweren und versuchen zu verhindern, dass er die beiden Versionen der Schleife unterschiedlich optimiert.

Ein Ansatz wäre, den Compiler zu zwingen, das „Hallo“ in einer Variablen zu speichern, die Sie in Flash zwingen.

Ich kenne den Mechanismus für MIPS nicht, aber es gibt sehr wahrscheinlich ein Pragma oder eine Möglichkeit, nach einer Variablen zu fragen, die vom Linker in das Flash-Segment des Programms eingefügt werden soll.

Für gcc für ARM kann eine Variable mit einer Anmerkung „geschmückt“ werden:
const uint8_t array[10] __attribute__((section(".eeprom"), used))
Dadurch wird die Variable so markiert, dass sie in den Abschnitt „.eeprom“ des Linkers eingefügt wird, und das Linkskript des Linkers stellt sicher, dass sich alle Adressen für diesen Abschnitt im Flash-Speicher befinden Adressbereich).

Möglicherweise müssen Sie jedoch auch die Compileroptimierungen umgehen, wenn Sie sie auf einen konstanten Zeichenfolgenwert anwenden.

Fügen Sie eine Allzweckversion der While-Schleife in eine separate Funktion ein, myfunc(a *char, b *char). Rufen Sie es dann mit zwei verschiedenen Sätzen von Variablen auf (RAM zu RAM vs. FLASH zu RAM). Dies sollte den Compiler normalerweise dazu zwingen, einen Codesatz (den Hauptteil von myfunc) zu generieren, der für beide Fälle verwendet wird. Das würde einen „Äpfel für Äpfel“-Vergleich ergeben. Unterschätzen Sie jedoch nicht die Optimierungsfähigkeit eines Compilers. Vielleicht möchten Sie den Assembler trotzdem ausgeben, um zu überprüfen, ob der Compiler nicht zu schlau ist.

(Ich würde die Anzahl der Iterationen begrenzen, um zu verhindern, dass Peripheriegeräte gekritzelt werden.)

All dies ist Spekulation. Sie müssen weitere Informationen bereitstellen, insbesondere die Initialisierung der CPU-Uhr, Busse und Puffer, den von Ihnen verwendeten Compiler und idealerweise einen Assembler-Dump, um genauere Antworten zu geben.

Es könnte jedoch ausreichen, Ihre Anforderung zu erfüllen, wenn Sie die Änderung an einer einzelnen Funktion vornehmen, die eine große for-Schleife ausführt, und diese mit zwei verschiedenen Parametersätzen aufrufen

Ich bin beeindruckt von Ihrer Hilfsbereitschaft und Ihrem profunden Wissen. Vielen Dank! Ich werde sehen, was ich aus dem Code der Assemblersprache machen kann. Das Problem, das ich bei diesem Ansatz habe, ist, dass die von mir verwendete IDE, die MPLAB X IDE, mir den zerlegten Code ohne beibehaltene Beschriftungen anzeigt. Ich möchte lieber, dass der Compiler Assemblercode direkt generiert, wobei die Beschriftungen beibehalten werden. Weiß jemand wie man das macht?
@VititKantabutra - Ich entschuldige mich, aber ich weiß nicht, wie ich MPLAB X IDE dazu bringen kann, "nützlichen" Assembler auszugeben. Diese Links könnten helfen microchip.com/forums/m537589.aspx stackoverflow.com/questions/24914860/…

Deine eigene Analyse ist richtig.

Die Funktion strcpy(&, "") für kleine Zeichenfolgen wird höchstwahrscheinlich für sechs aufeinanderfolgende LoadImmediate-Befehle optimiert, es sei denn, die Optimierung ist vollständig ausgeschaltet , wodurch ein bestimmter Wert auf ein Byte gesetzt wird (Register oder anderweitig, falls verfügbar). Mehr wird nicht optimiert, da der Quelltyp char ist, was Bytes ist. Aber 6 LDIs sind immer noch schneller als 6 * 2 * LD. Oder wenn es direktes RAM zu RAM unterstützt, könnte es in 6 * LD funktionieren, aber da Option 2 langsamer ist, wahrscheinlich nicht.

Wenn Sie möchten, dass die Zeichenfolge aus dem Flash-Datenraum stammt, besteht die einzige Garantie dafür darin, herauszufinden, wie Microchip ein Flash-Array definiert. Genau wie Ihre anderen globalen Variablen können Sie dem System mitteilen, dass es ein Array von Variablen im Flash erstellen soll.

Wie Sie das einrichten, hängt von Ihrer Umgebung ab. Sie sollten nach „Store Array in Flash PIC“ suchen, gefolgt vom Namen Ihres Compilers oder Ihrer Umgebung.

BEARBEITEN:

Um endloses Kommentieren abzuwehren: Ja, lange hartcodierte Zeichenfolgen können zu Flash-Arrays werden (in einigen Optimierungseinstellungen), aber es gibt keine strengen Regeln dafür, was allgemein als "lang" gilt. Also bleibe ich bei meiner Aussage "Die einzige Garantie ist ...".

  • Optimierungen deaktivieren.
  • Deklarieren Sie alle Arrays als flüchtig.

Erledigt.

Ich habe mir ein Microchip-Datenblatt für den PIC32MX5XX/6XX/7XX angesehen. In Tabelle 31-12: "PROGRAM FLASH MEMORY WAIT STATE CHARACTERISTIC", dort steht: - 0 Wartezustand 0 bis 30 MHz - 1 Wartezustand 31 bis 60 MHz - 2 Wartezustände 61 bis 80 MHz Also, das sind Flash-Wartezustände. Meiner Erfahrung nach haben CPUs mit "Befehlsraten" über 40 MHz (ignorieren Sie die Multi-Clock-/Befehls-CPUs) Flash-Speicher-Wartezustände. IIRC einer der japanischen MCU-Hersteller (Toshiba, Fujitsu?) hat einen viel schnelleren Flash als die durchschnittliche MCU m/w.
@gbulmer Ah, diesen Teil habe ich verpasst, danke. Je nach MCU-Takt kann es also tatsächlich zu einer Verzögerung für diesen Teil kommen. Ich denke jedoch, dass die aktuelle Benchmarking-Methode des OP viel zu grob wäre, um diese Wartezustände zu erfassen.
Ja, das Handbuch ist etwas umständlich, da der Abschnitt über Flash-Speicher nur die Flash-Programmierung erwähnt und das Flash-Verhalten eine Tabelle in einem riesigen Abschnitt über "elektrische Eigenschaften" ist. Ich habe eine Möglichkeit vorgeschlagen, einige der Compiler-Optimierungen zu umgehen und sicherzustellen, dass der Großteil der Schleife derselbe Code ist. Aber selbst das könnte zeigen, dass "Flash to RAM" schneller ist als "RAM to RAM", abhängig von der tatsächlich generierten MIPS-Codesequenz. IMHO ist es sinnvoll, den gleichen Schleifencode für jeden Fall neu zu schreiben, aber sehen wir uns den Assembler an. Andernfalls könnten wir wie Banker desaströs spekulieren.
@gbulmer Zuverlässiger Benchmarking-Code würde ungefähr so ​​​​aussehen, for(;;) PORT ^= variable;wenn "Variable" als deklariert ist volatileund sich entweder im RAM oder im ROM befindet. Der Overhead-Code (die Schleife und das XOR) ist minimal und von statischer Länge, sodass Sie die Menge der durch den Overhead verursachten CPU-Ticks durch Disassemblieren leicht zählen können.
AFAICT, das OP interessiert sich für die relative Geschwindigkeit von Flash und RAM. Ich habe den PIX32MX7xx nicht überprüft, aber Peripheriebusse sind oft viel langsamer als RAM und haben oft sehr wenig Pufferung (dh einen einzelnen Schreibpuffer). Also, IMHO for(;;) PORT ^= variable;kann nutzlos sein. Es kann einen Engpass beim Zugriff auf den Peripheriebus geben und dem OP sehr wenig über RAM vs. Flash sagen. Außerdem könnte der „Prefetch-Cache“ unsichtbar sein, sodass der Wert selbst dann, wenn der Compiler Ladeanweisungen für die residente Flash-Variable generiert, aus dem Cache bereitgestellt wird und genauso schnell wie RAM ist. Assembler sagt uns vielleicht nicht genug
@gbulmer Ach, verdammt noch mal, Datencache :) Ja dann wird es offensichtlich nicht funktionieren. Sie müssten dann einen halbwegs fortgeschrittenen Trick schreiben, um die 16 Cache-Zeilen zu überwinden, oder den Cache deaktivieren. Es scheint, dass dies durch Registerschreibvorgänge in das Cache-Steuerregister CHECON möglich wäre.
Der Prefetch-Fall ist für Flash, daher kann er den Zugriff auf eine Flash-residente Variable und Code beschleunigen, aber nicht (hilfreich) den Zugriff auf ein Peripheriegerät oder RAM beeinflussen. Ja, der Prefetch-Cache kann abgeschaltet werden. Dies könnte die Benchmark-Ergebnisse jedoch auf eine ganz andere, nicht repräsentative Weise verzerren. Bis wir weitere Informationen vom OP erhalten, werde ich aufhören, mir darüber Sorgen zu machen. Benchmarking ist hart, auch wenn wir uns daran erinnern, dass „Benchmarking hart ist“ :-)