Ausführungszeitproblem der Funktion Memset() in C

Ich habe eine Typedef-Struktur, die meine kumulativen ADC-Parameter speichert. Wenn ich meine ADC_Sum-Struktur lösche, dauert es weniger als 300 ns, um den Löschvorgang zwischen Größe 1-8 zu erreichen. Aber wenn die Größe größer als 8 ist, dauert es ungefähr 320 µs!!!

Ich habe festgestellt, dass es für eine Größe von 16 wieder weniger als 300 ns dauert.

Ich arbeite an einem STM32 Embedded Board. Ich frage mich, warum dies passiert, weil es meinen Echtzeitprozess beeinflusst. Sie finden die Zeittabelle und meinen Code unten.

Bagger

typedef struct
{
   float CH1;
   float CH2;
   float CH3;
   float CH4;
   float CH5;
   float CH6;
   float CH7;
   float CH8;
   float CH9;
   float CH10;
   float CH11;
   float CH12;
   float CH13;
   float CH14;
   float CH15;
   float CH16;
   float CH17;

} Typedef_ADC_Sum;

Typedef_ADC_Sum ADC_Sum;

void clearSumArray(void)
{
    memset(&ADC_Sum, 0, sizeof(ADC_Sum));
}

Geben Sie hier die Bildbeschreibung ein

Interessantes Ergebnis. Welche Entwicklungsplattform und Bibliotheken verwenden Sie? Es scheint, als würde der Compiler etwas tun, oder vielleicht eine schlecht implementierte Memset-Funktion. Können Sie sich die Disassemblierung für diese Funktion ansehen und sehen, ob das sie erklärt? Haben Sie versucht, Ihre eigene Clear-Funktion zu schreiben (durchlaufen Sie einfach die Elemente und löschen Sie jedes von ihnen), um zu sehen, wie die Geschwindigkeit im Vergleich dazu ist?
Ja. Ich habe versucht, alle Variablen einzeln zu löschen, wie ADC_Sum.CH1 = 0; ..... ADC_Sum.CH17 = 0. Es gibt kein Problem, wenn ich jedes Feld einzeln lösche.
Aber wie lange dauert es?
Ich bin mir nicht 100 * sicher, aber es könnte todo sein, dass Memset eine böse Funktion und eine knifflige Funktion ist: augias.org/paercebal/tech_doc/doc.en/cp.memset_is_evil.html viva64.com/en/b/0360
Sie würden die Antwort wahrscheinlich herausfinden, wenn Sie sich den vom Compiler generierten Assemblercode ansehen würden.
Ich nehme nicht an, dass der angegebene Teil einen begrenzten Datencache hat? Was soll "Memset-Funktionsgröße" bedeuten? Die Anzahl der an die Funktion übergebenen Bytes? Die Menge an Schwimmern?

Antworten (1)

Hier sind mehrere Faktoren zu berücksichtigen. Sie fallen vielleicht nur in einen von ihnen, aber Sie müssen trotzdem vorsichtig sein. In erster Linie ist es trivial, große Leistungsunterschiede mit einfachen zwei Befehlsschleifen zu demonstrieren:

top:
subs r0,r0,#1
bne top

Welche CPU, die bestimmen kann oder nicht, wie sie abruft, ruft normalerweise einen Block von Anweisungen in einer Bustransaktion ab, beispielsweise 4 oder 8 Anweisungen. Und dann fangen Sie an, an diesen zu arbeiten, wenn sie herausfinden, dass Sie rückwärts verzweigen, müssen sie möglicherweise keine weiteren 4 vorab abrufen. Wenn diese zwei Abrufzeilen umfassen, wie ich sie gerne nenne, müssen Sie diese die ganze Zeit abrufen ( vorausgesetzt, kein Cache, aber das gleiche Problem besteht mit einem aktivierten Cache, der Prozessor muss noch abrufen).

Nur diese beiden Anweisungen und ein einziger STM32 Ich kann die Leistung stark variieren lassen.

Das kann Teil des Problems sein oder auch nicht.

memset ist eine C-Bibliothekssache, also müssen Sie sich Ihre C-Bibliothek ansehen. Diese werden oft in Handarbeit in der Montage hergestellt, und Sie könnten denken, dass dies eine schlechte Umsetzung ist, aber ich bin bereits von Ihren Zahlen beeindruckt. Sie nutzen den Befehlssatz und die Ausrichtung. Ihr Array besteht aus 32-Bit-Dingen, also ist es an einer 32-Bit-Grenze ausgerichtet. Wenn Ihr Array ein Haufen Bytes wäre und Sie die Variablen davor optimieren könnten, könnten Sie es möglicherweise an diesem Ende nicht ausgerichtet machen, sicherlich am Backend, indem Sie es zu einer ungeraden Anzahl von Dingen machen.

Im Allgemeinen wird memset den weniger effizienten Byte-Befehl verwenden wollen, um zu einer Halbwortausrichtung zu gelangen, und wenn das nicht wortausgerichtet ist, dann ein Halbwortspeicher, um zu einer Wortausrichtung zu gelangen, und dann mit mehreren Wörtern pro Speicher eine Schleife durchlaufen, bis dies der Fall ist kurz vor dem Ende, aber nicht vorbei. Dann je nach Bedarf ein Zwei-Wort-Speicher, ein Ein-Wort-Speicher, ein Halbwort-Speicher und ein Byte-Speicher. Es ist wie bei einem Standardgetriebe, das durch die Gänge bis zum hohen Gang arbeitet und dort bleibt, solange Sie dann wieder herunterarbeiten können, um anzuhalten.

Die ersten acht wären Einzel-, Doppel- und Vierfach-Wortspeicher, da dies alles ausgerichtet ist, plus den Aufwand für die Vorbereitung der Register zum Ausführen des Speicherns.

Es gibt Vorbereitungsarbeit sowohl beim Nullstellen einiger Register, die zum Speichern von Nullen verwendet werden sollen, als auch etwas Arbeit, um die Ausrichtung an jedem Ende zu bestimmen.

mov r4,#0
str r4,[r0]

mov r4,#0
mov r5,#0
std r4,[r0]

mov r4,#0
mov r5,#0
std r4,[r0],#8
str r4,[r0]

mov r4,#0
mov r5,#0
std r4,[r0],#8
std r4,[r0]

Arbeiten Sie sich bis zu Schleifen von,

stm r0!,{r4,r5,r6,r7}

oder sogar abgerollte Schleifen von

stm r0!,{r4,r5,r6,r7}
stm r0!,{r4,r5,r6,r7}
stm r0!,{r4,r5,r6,r7}
stm r0!,{r4,r5,r6,r7}

und dann bei Bedarf runterschalten.

Sie verwenden eine generische C-Bibliothek, die wahrscheinlich vorkompiliert und repariert wurde und den Overhead hat, wenn eine beliebige Ausrichtung der Adresse und eine beliebige Anzahl von Bytes angenommen wird. Sie machen ausgerichtete Wörter, damit Sie etwas schneller von Hand herstellen können. Sie fallen immer noch in das Problem der Befehlsausrichtung, aber Sie können das auch durcharbeiten ...

Ich würde mit Sachen anfangen wie

two_word_fill:
   stm r1!,{r2,r3}
   subs r0,r0,#1
   bne two_word_fill:

und nenne das mit

two_word_fill(13,ADC_Sum,0,0);

den C-Compiler r2 und r3 für uns vorbereiten zu lassen. Der Stack wird auf der Assembly-Seite nicht benötigt, aber wahrscheinlich auf der C-Seite, um die Funktion aufzurufen. Sie können das nicht umgehen, es sei denn, Sie bauen die gesamte Funktion, in der diese lebt, von Hand zusammen (oder spielen Inline-Assembly-Spiele).

Du könntest so etwas versuchen:

test2:
   push {r4,r5,r6,r7}
   mov r4,#0
   mov r5,#0
   mov r6,#0
   mov r7,#0
t2loop:
   stm r0!,{r4,r5,r6,r7}
   stm r0!,{r4,r5,r6,r7}
   stm r0!,{r4,r5,r6,r7}
   stm r0!,{r4,r5,r6,r7}
   subs r1,r1,#1
   bne t2loop
   pop {r4,r5,r6,r7}
   bx lr

Nennen Sie es mit:

test2(ADC_Sum,n);

Es macht 16 Wörter pro n, wenn Ihr Array also 16 tief wäre, würden Sie eine 4 für n verwenden. Machen Sie Ihre Struktur idealerweise zu einem Vielfachen von 16 Wörtern.

Sie können die Ausrichtung des Codes manuell anpassen, indem Sie die Assembler-Syntax verwenden, die für Gas ungefähr so ​​​​aussieht:

.align 8
nop
nop
top:
subs r0,r0,#1
bne top

Bringen Sie es dazu, es an einer ausreichend großen Grenze auszurichten (die 8 bedeutet nicht unbedingt, dass 8 Wörter mit unterschiedlichen Werten spielen, und sehen Sie sich die Zerlegung des verknüpften Ergebnisses an). Durch Hinzufügen von Nops können Sie dann einstellen, wo in den Abrufzeilen Ihre Hauptschleife lebt.

Ihr Array ist ziemlich klein, sodass Sie wahrscheinlich auf ein weiteres Problem mit dem genauen Timing Ihres Tests stoßen werden. Außerdem machen das Problem des Setups, das erforderlich ist, um in die Hochgeschwindigkeitsschleife zu gelangen, und die Bereinigung am Ende, falls vorhanden, einen größeren Prozentsatz der Gesamtzeit aus wenn das Array kleiner wird. Wenn Sie sich Memset- und Memcpy-handgefertigten Code ansehen, stellen Sie fest, dass sie manchmal einen Schwellenwert haben. Wenn weniger als N, zum Teufel damit; machen Sie einfach eine Schleife von Bytespeichern. Wenn Sie diese Schwelle überschritten haben, versuchen wir etwas anderes und so weiter. Vielleicht sehen Sie das bereits bei Ihren Tests. Die 16 Wörter sind leicht zu erkennen und gehen schneller, und die zwischen 8 und 16 können Leistungsschmerzen verursachen. Die 8 und darunter können in sein. Es ist nur schneller, eine Zeichenspeicherschleife zu N auszuführen und den Abschnitt des Codes zu erledigen.

Wenn dies also wirklich so zeitkritisch ist, sollten Sie keinen generischen C-Bibliotheksaufruf verwenden, der sich zu irgendeinem Zeitpunkt in der Zukunft mit der Toolchain oder Bibliotheks-Upgrades ändern kann, was in Zukunft zu Wartungsalpträumen führen kann. Sie müssen dieses besitzen, wenn Sie unbedingt die Zeit herunterbekommen müssen.

Beachten Sie, dass Sie dies wahrscheinlich auch vom Flash aus ausführen, nehme ich an. Flash ist auf diesen Geräten langsam und wenn Sie die Taktrate erhöhen, steigen die Flash-Wartezustände, sodass Ihre Flash-Leistung im Verhältnis zur Hauptuhr in einem gewissen Bereich ihrer maximalen Geschwindigkeit liegen kann, aber der RAM neigt dazu, keine Wartezustände zu haben. Wenn Sie diesen Code in den Arbeitsspeicher kopieren und von dort aus ausführen, steigt Ihre Leistung, wenn nicht sogar um das Doppelte.