Schnellste Abfrageschleife - wie kann ich 1 CPU-Zyklus kürzen?


8

In einer Echtzeitanwendung¹ auf einem ARM Cortex M3 (ähnlich wie STM32F101) muss ich ein Stück des Registers eines internen Peripheriegeräts abfragen, bis es Null ist, und zwar in einer möglichst engen Schleife. Ich benutze Bitbanding, um auf das entsprechende Bit zuzugreifen. Der (Arbeits-) C-Code lautet

while (*(volatile uint32_t*)kMyBit != 0);

Dieser Code wird in den ausführbaren RAM auf dem Chip kopiert. Nach einigen manuellen Optimierungen² ist die Abfrageschleife auf Folgendes zurückzuführen, das ich auf 6 Zyklen eingestellt habe:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 D1FC      BNE      0x00600200

Wie kann die Wahlunsicherheit verringert werden? Eine 5-Zyklus-Schleife würde zu meinem Ziel passen: Das gleiche Bit so nahe wie möglich an 15,5 Zyklen abtasten, nachdem es auf Null gegangen ist.

Meine Spezifikation fordert die zuverlässige Erkennung eines niedrigen Impulses von mindestens 6,5 CPU-Taktzyklen; zuverlässig als kurz klassifizieren, wenn es weniger als 12,5 Zyklen dauert; und zuverlässig klassifizieren, solange es länger als 18,5 Zyklen dauert. Die Impulse haben keine definierte Phasenbeziehung zum CPU-Takt, was meine einzige genaue Zeitreferenz ist. Dies erfordert eine Abfrageschleife von höchstens 5 Takten. Eigentlich emuliere ich Code, der auf einer jahrzehntealten 8-Bit-CPU lief, die mit einem 5-Takt-Zyklus abrufen konnte, und was das tat, wurde zur Spezifikation.


Ich habe versucht, die Code-Ausrichtung durch Einfügen von NOP vor der Schleife in den vielen Varianten, die ich ausprobiert habe, auszugleichen, habe jedoch nie eine Änderung festgestellt.

Ich habe versucht, CMP und LDR zu invertieren, erhalte aber immer noch 6 Zyklen:

0x00600200 681A      LDR      r2,[r3,#0x00]
; we loop here
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FC      BNE      0x00600202

Dieser ist 8 Zyklen

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 681A      LDR      r2,[r3,#0x00]
0x00600204 2A00      CMP      r2,#0x00
0x00600206 D1FB      BNE      0x00600200

Aber dieser ist 9 Zyklen:

0x00600200 681A      LDR      r2,[r3,#0x00]
0x00600202 2A00      CMP      r2,#0x00
0x00600204 681A      LDR      r2,[r3,#0x00]
0x00600206 D1FB      BNE      0x00600200

¹ Messen, wie lange das Bit niedrig ist, in einem Kontext, in dem kein Interrupt auftritt.

² Der ursprünglich vom Compiler generierte Code verwendete r12 als Zielregister und fügte der Schleife 4 Codebytes hinzu, was 1 Zyklus kostete.

³ Die angegebenen Zahlen werden mit einem vermeintlich zyklengenauen Echtzeit- STIce-Emulator und seiner Emulator-Triggerfunktion beim Lesen an der Adresse des Registers erhalten. Früher habe ich den "States" -Zähler mit einem Haltepunkt in der Schleife ausprobiert, aber das Ergebnis hängt von der Position des Haltepunkts ab. Einzelschritt ist noch schlimmer: Es gibt immer 4 Zyklen für LDR, wenn das mindestens irgendwann bis zu 3 ist.


Die Ausrichtung kann von Bedeutung sein. Die GPIO-Taktdomäne kann sowohl die Leistung als auch die Flash-Wartezustände dominieren. Es handelt sich um 3+ Uhren, jedoch je nach 6+ oder sogar mehr. Ich würde erwarten, dass Bit-Banding kein Leistungseinbruch für Lesevorgänge ist, aber Sie könnten das Bit testen, anstatt es auf Null zu vergleichen und zu sehen. Unterm Strich muss man es einfach versuchen ...
old_timer

1
Hat der Thumb-Modus nicht einen cbnz-Befehl zum Vergleichen und Verzweigen in einem anderen Register, das Null ist? Hast du mit kompiliert gcc -Os -mcpu=cortex-m3?
Peter Cordes

1
@ Peter Cordes: Ich verwende nicht gcc, sondern ArmCC 5 (ARMs Compiler der vorherigen Generation, bevor ich zu LLVM gehe). Die Optimierung ist zeitlich und maximal, und die Optionen für die CPU sollten von der IDE automatisch festgelegt werden, aber ich werde das überprüfen. Ja, es gibt CBZ / CBNZ, aber wenn ich das Dokument lese , kann es nicht rückwärts verzweigen.
Fgrieu

1
Ok, Sie (oder der Compiler) könnten sich mit ldr/ cbz reg, end_of_loopfür die inneren abrollen und immer noch ein cmp/ bnzam unteren Rand. Dies würde Ihnen jedoch ein ungleichmäßiges Abfrageintervall geben, z. B. 1 von 8 Umfragen, falls dies von Bedeutung ist.
Peter Cordes

2
Sind Sie sicher, dass Sie die Spezifikation nicht falsch interpretiert haben? Möglicherweise könnte die Spezifikation, wenn sie sich auf "gerätespezifische Zyklen bezieht, die keine CPU-Zyklen sind" (z. B. ein Timer oder UART mit einer eigenen Taktquelle, die viel langsamere Zyklen aufweist), und möglicherweise "so kurz wie 13 gerätespezifische Zyklen" "so kurz sein als 13000 CPU-spezifische Zyklen ".
Brendan

Antworten:


8

Wenn ich die Frage richtig verstehe, müssen nicht unbedingt die Schleifenzyklen reduziert werden, sondern die Anzahl der Zyklen zwischen aufeinander folgenden Abtastwerten (dh LDR-Anweisungen). Es kann jedoch mehr als einen LDR pro Iteration geben. Sie können so etwas ausprobieren:

    ldrb    r1, [r0]

loop:
    cbz     r1, out
    ldrb    r2, [r0]
    cbz     r2, out
    ldrb    r1, [r0]
    b       loop

out:

Der Abstand zwischen den beiden LDRB-Befehlen variiert, sodass die Abtastwerte nicht gleichmäßig verteilt sind.

Dies kann das Verlassen der Schleife etwas verzögern, aber anhand der Problembeschreibung kann ich nicht sagen, ob es wichtig ist oder nicht.

Ich habe zufällig Zugriff auf das zyklusgenaue M7-Modell, und wenn der Prozess Ihre ursprüngliche Schleife stabilisiert, läuft sie auf M7 in 3 Zyklen pro Iteration (dh LDR alle 3 Zyklen), während die oben vorgeschlagene Schleife in 4 Zyklen ausgeführt wird, aber jetzt gibt es sie zwei LDRs drin (also LDR alle 2 Zyklen). Die Abtastrate ist definitiv verbessert.

@Peter Cordes schlug in einem Kommentar vor , sich mit CBZ als Pause abzuwickeln .

Zugegeben, M3 wird langsamer sein, aber es ist immer noch einen Versuch wert, wenn es die Abtastrate ist, nach der Sie suchen.

Sie können auch überprüfen, ob LDRB anstelle von LDR (wie im obigen Code) etwas ändert, obwohl ich dies nicht erwarte.

UPD: Ich habe eine andere 2-LDR-Schleifenversion, die auf M7 in 3 Zyklen abgeschlossen ist, die Sie aus Interesse ausprobieren können (auch CBZ-Unterbrechungen ermöglichen ein einfaches Ausbalancieren der Pfade nach der Schleife):

    ldr     r1, [r0]

loop:
    ldr     r2, [r0]
    cbz     r1, out_slow
    cbz     r2, out_fast
    ldr     r1, [r0]
    b       loop

out_fast:
    /* NOPs as required */

out_slow:

Gute Nachricht: Das läuft mit 10 Zyklen / Schleife mit 2 Abtastungen, daher ist die durchschnittliche Abtastrate in Ordnung. Ernstes Problem: Die Verzögerung zwischen den Samples beträgt 4 Zyklen von dem r2zu dem zu gehen r1, aber 6 Zyklen von dem r1zu dem zu gehen r2(aufgrund des b loopdazwischen liegenden), wenn ich (höchstens) 5 möchte Zyklen zwischen den Probenahmen. Ein leicht zu behebendes Problem besteht darin, dass wir outnach einer größeren Verzögerung nach dem Abtasten erreichen, wenn der Ausgang r1Null ist, als wenn er r2Null ist. In anderen Nachrichten wurde ldrbvon einer Bit-Banding-Adresse Probleme verursacht, geändert zu ldr.
Fgrieu

2
Ich bin froh, dass es funktioniert hat :) Ich wollte nicht in den Ausgleich der Samples gehen, bevor ich bestätigt habe, dass die Grundidee für Sie funktioniert hat. Es ist auch schwer vorherzusagen, wie meine M7-Läufe in M3 übersetzt werden. Ich habe Ihnen diese Loop-Version gegeben, weil sie auf M7 eine einheitliche Abtastrate erzeugt (LDR alle 2 Zyklen). Aber ich hatte auch eine andere Version, die auf M7 tatsächlich schneller lief (3 Zyklen pro Schleife und immer noch 2 LDRs). Sie können sie aus Interesse ausprobieren. Ich werde meine Antwort aktualisieren.
alex_mv

3
Ich bestätige, dass Ihre zweite Abfrageschleife in 10 Zyklen mit zwei gleich beabstandeten Proben auf meinem Cortex-M3-Emu ausgeführt wird. Es trimmt meine Antwort (jetzt entfernt) auf Einfachheit und ermöglicht es, kürzere Impulse zu testen. 2 nopvor loop:und 4 nopnach out_fast:macht es so, dass a ldrnach out_slow:Proben 10 Zyklen nach der Probe, die zuerst bei Null gesehen wurde, welcher der drei war. Meine Spezifikation (wie in der Frage formuliert) erfordert 13, und das ist trivial anzupassen. Problem 100% gelöst! Vielen Dank sowie an Peter Cordes für seinen Kommentar und B Degan für das erste Kopfgeld.
fgrieu

@fgrieu: oh ja, das ist ein kluger Trick im neuesten Update. out_fastkönnte vielleicht kompakter als 5 nops sein, vielleicht nur einen anderen ldrausführen oder vielleicht einen bnächsten Befehl ausführen, wenn dies mehr Zyklen als ein NOP auf Ihrer CPU dauert, ohne die Verzweigungsvorhersage (falls vorhanden) zu verschmutzen.
Peter Cordes

1

Sie können dies versuchen, aber ich bin misstrauisch, dass es die gleichen 6 Zyklen geben wird

0x00600200 581a      LDR      r2,[r3,r0]; initialize r0 to 0x0
0x00600202 4282      CMP      r2,r0
0x00600204 D1FC      BNE      0x00600200

4
Ich habe versucht: 6 Zyklen :-(
fgrieu
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.