Wie erzwinge ich ein unbenutztes Speicherlesen in C, das nicht wegoptimiert wird?

Lesezeit: 8 Minuten

Benutzer-Avatar
Richter Maygarden

Mikrocontroller erfordern häufig das Lesen eines Registers, um bestimmte Statusbedingungen zu löschen. Gibt es eine portable Methode in C, um sicherzustellen, dass ein Lesevorgang nicht wegoptimiert wird, wenn die Daten nicht verwendet werden? Reicht es aus, dass der Zeiger auf das speicherabgebildete Register als flüchtig deklariert ist? Mit anderen Worten, würde Folgendes immer auf standardkonformen Compilern funktionieren?

void func(void)
{
   volatile unsigned int *REGISTER = (volatile unsigned int *) 0x12345678;

   *REGISTER;
}

Ich verstehe, dass der Umgang mit Funktionen wie dieser zu Compiler-abhängigen Problemen führt. Meine Definition von tragbar ist in diesem Fall also etwas locker. Ich meine nur, dass es mit den gängigsten Toolchains so weit wie möglich funktionieren würde.

  • Die Inline-Montage ist normalerweise nicht optimiert. Wenn Sie es nicht plattformübergreifend wollen, ist es in Ordnung.

    – Dmytro Sirenko

    11. Dezember 2012 um 16:06 Uhr


  • @EarlGray Das ist eine sehr gültige Technik. Allerdings, und das ist pingelig, habe ich ein Beispiel, wo das eine zusätzliche Anweisung erfordern würde, weil der Compiler andernfalls das Laden einer Adresse optimieren könnte, indem er ein relatives Lesen von einer bereits geladenen Adresse verwendet. Natürlich habe ich Portabilität als Ziel angegeben und nicht Leistung …

    – Richter Maygarden

    11. Dezember 2012 um 16:16 Uhr


  • @Judge: FYI, es gibt eine weitere Besorgnis über den Unterschied zwischen dem Zugriff auf eine flüchtige Datei Objektund Zugriff auf ein Objekt mit einem flüchtigen lvalue-Ausdruck (stackoverflow.com/questions/13268657). Diese Frage bezieht sich auf C++, aber die gleiche Spitzfindigkeit könnte auch für C gelten.

    – Steve Jessop

    11. Dezember 2012 um 16:56 Uhr


  • Ich rufe eine in Assembly geschriebene Funktion GET32(addr) ldr r0 auf,[r0]; bx lr und habe nie Probleme. Ist portabler als Inline, natürlich etwas langsamer, hat aber andere Vorteile.

    – Oldtimer

    11. Dezember 2012 um 22:52 Uhr

Benutzer-Avatar
zwöl

Die Leute streiten ziemlich heftig darüber, was genau volatile meint. Ich denke, die meisten Leute stimmen darin überein, dass das Konstrukt, das Sie zeigen, war beabsichtigt zu tun, was Sie wollen, aber es gibt keine allgemeine Vereinbarung, dass die Sprache im C-Standard ist garantiert es eigentlich ab C99. (Möglicherweise hat sich die Situation in C2011 verbessert; das habe ich noch nicht gelesen.)

Eine nicht standardmäßige, aber von eingebetteten Compilern ziemlich weithin unterstützte Alternative, die möglicherweise eher funktioniert, ist

void func(void)
{
  asm volatile ("" : : "r" (*(unsigned int *)0x12345678));
}

(Das ‘volatile’ bezieht sich hier auf das ‘asm’ und bedeutet ‘dieses darf nicht gelöscht werden, obwohl es keine Ausgangsoperanden hat. Es ist nicht notwendig, es auch auf den Zeiger zu legen.)

Der größte verbleibende Nachteil dieses Konstrukts besteht darin, dass Sie immer noch keine Garantie dafür haben, dass der Compiler eine generiert eine Anweisung Speicher gelesen. Mit C2011 mit _Atomic unsigned int könnte ausreichen, aber in Ermangelung dieser Funktion müssen Sie so ziemlich selbst einen echten (nicht leeren) Montageeinsatz schreiben, wenn Sie diese Garantie benötigen.

BEARBEITEN: Heute Morgen ist mir noch eine Falte aufgefallen. Wenn das Lesen aus dem Speicherplatz den Nebeneffekt hat, dass der Wert an diesem Speicherplatz geändert wird, müssen Sie

void func(void)
{
  unsigned int *ptr = (unsigned int *)0x12345678;
  asm volatile ("" : "=m" (*ptr) : "r" (*ptr));
}

um eine Fehloptimierung anderer Lesevorgänge von diesem Ort zu verhindern. (Um es 100 % klarzustellen, diese Änderung ändert nicht die Assemblersprache, für die generiert wird func selbst, kann aber die Optimierung des umgebenden Codes beeinträchtigen, insbesondere wenn func ist eingebettet.)

  • Ich kenne das Konstrukt nicht. Wie würde es aussehen, wenn die Adresse in einer Pointer-Variable statt in einer Konstanten wäre?

    – Richter Maygarden

    11. Dezember 2012 um 16:24 Uhr


  • @JudgeMaygarden ... : : "r" (*ptr). Die Klammern sind obligatorisch, aber was drin ist, ist nur ein normaler alter C-Ausdruck.

    – zol

    11. Dezember 2012 um 16:29 Uhr


  • Vielen Dank. Ich habe gerade herausgefunden, dass mir die Klammern um den Zeiger fehlten.

    – Richter Maygarden

    11. Dezember 2012 um 16:29 Uhr


  • Das funktioniert. Es ermöglicht auch, dass das Laden der Adresse aus dem Zeiger herausoptimiert wird. Ich hätte nicht gedacht, dass die zuverlässigste und portabelste Version Inline-Assembler verwenden würde. Das Setzen des Leseparameters ohne eigentliche Anweisung ist ein netter Trick!

    – Richter Maygarden

    11. Dezember 2012 um 16:33 Uhr

  • Ich habe dies in ein Makro verpackt und es funktioniert genauso wie mein Beispiel mit IAR EWARM. Ich überlege, in Zukunft zu GCC zu wechseln, und möchte lose Enden stützen. Ich muss noch beide Methoden in GCC testen.

    – Richter Maygarden

    11. Dezember 2012 um 16:44 Uhr

Benutzer-Avatar
Lundin

Ja, der C-Standard garantiert, dass Code, der auf eine flüchtige Variable zugreift, nicht wegoptimiert wird.

C11 5.1.2.3/2

“Zugriff auf ein flüchtiges Objekt, ” … “sind alles Nebeneffekte”

C11 5.1.2.3/4

“Eine tatsächliche Implementierung muss einen Teil eines Ausdrucks nicht auswerten, wenn daraus abgeleitet werden kann, dass sein Wert nicht verwendet wird und dass keine erforderlichen Nebenwirkungen erzeugt werden (einschließlich solcher, die durch den Aufruf einer Funktion oder den Zugriff auf ein flüchtiges Objekt verursacht werden).”

C11 5.1.2.3/6

„Die Mindestanforderungen an eine konforme Implementierung sind:

— Zugriffe auf flüchtige Objekte werden streng nach den Regeln der abstrakten Maschine ausgewertet.”

  • Soweit ich mich erinnere, war diese Sprache alles in C99. Behandelt C2011 den spezifischen Punkt dessen, was als “Zugriff” qualifiziert wird? Das war in C99 implementierungsdefiniert, und das war der Streitpunkt, den Rodrigo, Steve Jessop und ich in den Kommentaren zu Rodrigos Antwort wiederholten.

    – zol

    12. Dezember 2012 um 16:45 Uhr


  • @Zack Nein, das 3. Zitat war nicht in C99, ich habe es überprüft, bevor ich es gepostet habe. Ich habe den Teil, der besagt, dass das, was als Zugriff gilt, implizit definiert ist, nicht zitiert.

    – Ludin

    12. Dezember 2012 um 18:46 Uhr

  • Meine Kopie von C99 ist zu Hause und ich bin es nicht, aber ich bin es sicher dass der Satz “Zugriffe auf flüchtige Objekte werden streng nach den Regeln der abstrakten Maschine ausgewertet” irgendwo in C99 aufgetaucht ist.

    – zol

    12. Dezember 2012 um 19:08 Uhr

  • @Zack Ah ja, du hast Recht, ich habe es bei C99 6.7.3/6 gefunden. Therefore any expression referring to such an [volatile] object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Es scheint, dass sie diesen Teil in C11 lediglich nach 5.1.2.3 verschoben haben.

    – Ludin

    12. Dezember 2012 um 19:20 Uhr

Benutzer-Avatar
rodrigo

IIRC, der C-Standard ist ein bisschen locker in der Definition der Verwendung, so die *REGISTER wird nicht unbedingt so interpretiert, als würde man a tun lesen.

Aber folgendes sollte reichen:

int x = *REGISTER;

Das heißt, das Ergebnis der Speicherreferenz muss irgendwo verwendet werden. Das x muss jedoch nicht volatil sein.

AKTUALISIEREN: Um die Warnung der _unused-Variablen zu vermeiden, könnten Sie eine No-Op-Funktion verwenden. Eine statische und/oder Inline-Funktion sollte ohne Laufzeiteinbußen wegoptimiert werden:

static /*inline*/ void no_op(int x)
{ }

no_op(*REGISTER);

AKTUALISIERUNG 2: Mir ist gerade eine nettere Funktion eingefallen:

static unsigned int read(volatile unsigned int *addr)
{
    return *addr;
}

read(REGISTER);

Nun kann diese Funktion sowohl für Read-and-Use als auch für Read-and-Discard verwendet werden. 😎

  • Wenn x selbst nicht verwendet wird, reicht dies möglicherweise nicht aus.

    – zol

    11. Dezember 2012 um 16:02 Uhr

  • Dies würde eine Warnung für eine nicht verwendete Variable ausgeben. Ich erlaube keine Warnungen in veröffentlichtem Code, und das ist gut, um Fehler zu finden. Dies würde das Deaktivieren dieser Warnung in Funktionen erfordern, die diese Technik verwenden.

    – Richter Maygarden

    11. Dezember 2012 um 16:04 Uhr

  • Ich sage es nur ungern, aber … ein ausreichend aggressiver Compiler wird Ihren optimieren no_op Funktion und optimieren Sie dann die Last auf die jetzt nicht verwendeten x.

    – zol

    11. Dezember 2012 um 16:10 Uhr


  • @Zack: Selbst wenn der Compiler das Schreiben aggressiv optimiert, kann er das Lesen nicht optimieren, da es flüchtig ist! Das Problem ist, dass das nirgendwo in den C-Spezifikationen steht *x meint read the value at x; es bedeutet nur einen lvalue mit dem x die Anschrift. Es ist offensichtlich, wenn Sie schreiben *x = 0sollte es nicht lesen *x bevor Sie hineinschreiben.

    – rodrigo

    11. Dezember 2012 um 16:21 Uhr


  • @Zack Wie in dieses Papier? Interessante Lektüre, nichtsdestotrotz.

    – Ludin

    12. Dezember 2012 um 18:49 Uhr

Vielleicht werden GNU C-spezifische Erweiterungen nicht als sehr portabel angesehen, aber hier ist eine andere Alternative.

#define read1(x)  \
({ \
  __typeof(x) * _addr = (volatile __typeof(x) *) &(x); \
  *_addr; \
})

Dies wird in die folgende Assembler-Zeile übersetzt (kompiliert mit gcc x86 und optimiert mit -O2):
movl SOME_REGISTER(%rip), %eax?

Ich bekomme den gleichen Assembler von:

inline read2(volatile uint32_t *addr) 
{ 
   return *addr; 
}`

… wie in einer anderen Antwort vorgeschlagen, aber read1() verarbeitet unterschiedliche Registergrößen. Auch wenn ich nicht sicher bin, ob ich es verwenderead2() mit 8- oder 16-Bit-Registern jemals ein Problem darstellen, es gibt zumindest keine Warnungen zum Parametertyp.

Compiler optimieren normalerweise keine Assembly-Inlines (es ist schwierig, sie richtig zu analysieren). Darüber hinaus scheint es eine geeignete Lösung zu sein: Sie möchten eine explizitere Kontrolle über die Register und es ist natürlich für die Montage.

Da Sie einen Mikrocontroller programmieren, gehe ich davon aus, dass Ihr Code bereits Assembler enthält, sodass ein bisschen Inline-Assembler kein Problem darstellt.

  • Das OP fragte: “Gibt es einen tragbaren Weg …”. Inline-Assembler kommt also überhaupt nicht in Frage.

    – Ludin

    12. Dezember 2012 um 15:23 Uhr

  • @Lundin Das OP sagt, dass “… meine Definition von tragbar in diesem Fall etwas locker ist. Ich meine nur, dass es mit den beliebtesten Toolchains so weit wie möglich funktionieren würde”. Mein Weg ist zumindest über Toolchains hinweg tragbar (und die akzeptierte Antwort bestätigt meine Meinung)

    – Dmytro Sirenko

    12. Dezember 2012 um 15:31 Uhr

  • Da Inline-Assembler nicht vom C-Standard abgedeckt wird (anders als in C++), wäre er auch nicht zwischen Tools portierbar. Es gibt asm NOP; und __asm NOP; und asm NOP und asm ("NOP") und asm { NOP; } usw…

    – Ludin

    12. Dezember 2012 um 15:36 Uhr

  • Das OP fragte: “Gibt es einen tragbaren Weg …”. Inline-Assembler kommt also überhaupt nicht in Frage.

    – Ludin

    12. Dezember 2012 um 15:23 Uhr

  • @Lundin Das OP sagt, dass “… meine Definition von tragbar in diesem Fall etwas locker ist. Ich meine nur, dass es mit den beliebtesten Toolchains so weit wie möglich funktionieren würde”. Mein Weg ist zumindest über Toolchains hinweg tragbar (und die akzeptierte Antwort bestätigt meine Meinung)

    – Dmytro Sirenko

    12. Dezember 2012 um 15:31 Uhr

  • Da Inline-Assembler nicht vom C-Standard abgedeckt wird (anders als in C++), wäre er auch nicht zwischen Tools portierbar. Es gibt asm NOP; und __asm NOP; und asm NOP und asm ("NOP") und asm { NOP; } usw…

    – Ludin

    12. Dezember 2012 um 15:36 Uhr

1355590cookie-checkWie erzwinge ich ein unbenutztes Speicherlesen in C, das nicht wegoptimiert wird?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy