MCU-Programmierung - C++ O2-Optimierung unterbricht While-Schleife

Ich weiß, dass Leute sagen, dass Code-Optimierung nur den versteckten Fehler im Programm hervorbringen sollte, aber hören Sie mir zu. Ich bleibe auf einem Bildschirm, bis eine Eingabe über einen Interrupt erfolgt.

Folgendes sehe ich im Debugger. Beachten Sie die überprüfte Zeile und den abgefangenen Ausdruckswert.Geben Sie hier die Bildbeschreibung ein

Code im Bild:

//...
ui::Context::addEventListener(ui::EventType::JOYSTICK_DOWN, &constant_dynamic_handler);

while (true) {
    if (choice != 0) //debugger pause
        break;
}

ui::Context::removeEventListener(ui::EventType::JOYSTICK_DOWN, &constant_dynamic_handler);

if (choice == 1) goto constant;
else if (choice == 2) goto dynamic;
else if (choice == 3) goto reset;
else if (choice == 4) goto exit;
//...

//debugger view: 
//expression: choice
//value: 1

Das constant_dynamic_handlerist eine zuvor deklarierte Lambda-Funktion, die sich nur choicein eine andere ganze Zahl als ändert 0. Dadurch, dass ich in der Schleife pausieren kann, wird die Schleife nicht verlassen, sondern der Wert verändert. Ich kann im Debugger keinen Schritt überspringen, da der Speicher auf der CPU nicht gelesen werden kann und ein Neustart erforderlich ist, um erneut zu debuggen.

choicewird einfach im gleichen Geltungsbereich wie der if-Anweisungsblock deklariert, as int choice = 0;. Es wird nur innerhalb eines Interrupt-Listeners geändert, der mit einem Hardwareeingang ausgelöst wird.

Das Programm arbeitet mit O0Flag statt O1or O2.

Ich verwende NXP K60 und c ++ 11, falls dies erforderlich ist. Ist es mein Problem? Kann es sein, dass mir etwas nicht bewusst ist? Ich bin ein Anfänger in der MCU-Programmierung, und dieser Code funktioniert auf dem Desktop(Gerade probiert, geht nicht).

Haben Sie es mit Optimierungen auf Ihrem Desktop kompiliert?
Ich würde auch nicht erwarten, dass dies auf einem Desktop-System funktioniert. C-Compiler dürfen eine Variable einmal lesen und dann davon ausgehen, dass sie sich nicht ändert, es sei denn, sie ist als flüchtig deklariert. Jeder Compiler, den ich in den letzten 20 Jahren verwendet habe, führt diese Optimierung durch.
Andere haben bereits auf den Grund hingewiesen, warum das mit nicht funktioniert -O2. Ich würde auch (aus Gründen des Stils) empfehlen, die Schleife zu vereinfachen - warum eine Schleife für immer ausführen und dann unterbrechen, wenn eine Bedingung erfüllt ist, anstatt nur zu tun while (choice == 0) {}?
Postleitzahl, keine Bilder des Codes.
Wie wird choicedeklariert?
@psmears Ich hatte das, bevor ich darauf umgestiegen bin, weil das nicht funktioniert hat.
@Wilson Das Bild, das ich bereitgestellt habe, zielte nur darauf ab, meinen Standpunkt mit dem zu beweisen, was ich während des Debuggens gesehen habe.
Welche Version von C++ verwendest du? C++11 führte einige neue Annahmen über Schleifen wie while(true) { /*...*/ }ein, die zusätzliche Kopfschmerzen verursachen könnten ... (Siehe diesen Teil eines Vortrags für ein relevantes Beispiel.)
@hoffmale C++11 in der Tat.
@ Jules Du hast Recht. Ich habe das nie ausprobiert. Und jetzt weiß ich, dass es auch auf dem Desktop nicht funktioniert.
Nicht im Zusammenhang mit der spezifischen Frage, aber "wird als schädlich angesehen" (google it). Wenn Sie erwarten, dass sich jemand anderes diesen Code ansieht, sollten Sie einen wirklich guten Grund für diese Gotos haben, andernfalls sollten Sie diesen Code neu schreiben. Bei mehreren Bedingungen für dieselbe Variable ist eine switch-Anweisung eleganter (und schneller, wenn der Compiler nicht erkennt, dass er die if-Anweisung bei der Optimierung in eine switch-Anweisung umwandeln kann). Auch Ihre While-Schleife könnte einfach "while (choice != 0);" geschrieben werden.
@Graham Der Grund, warum ich gotohier verwendet habe, ist, dass die vorherige Version nicht funktioniert hat. Ich habe alle möglichen Möglichkeiten ausgeschöpft, um den Code zu schreiben, der den gleichen Effekt hat.
(1) Während der Code selbst hilfreich sein kann, ist es hilfreicher, den Code selbst zu posten, damit die Leute ihn testen können. Siehe meta.stackoverflow.com/q/303812 . (2) Bitte posten Sie ein vollständiges, verifizierbares Minimalbeispiel , das die Deklaration von enthält choice, damit Sie keine Kommentare wie diese erhalten . Es ist durchaus möglich, dass der Fehler nicht in dem von Ihnen gezeigten Code liegt, obwohl sich das Programm dort anscheinend falsch verhält, können Sie vor langer Zeit ein undefiniertes Verhalten aufgerufen haben.
Dies wäre viel besser beim Stapelüberlauf gefragt worden - wo Sie viel mehr engagierte C++-Leute haben.
Ihre Frage diskriminiert Blinde und Sehbehinderte. Bitte poste den aktuellen Code im Textformat .
Beachten Sie, dass Debugger und Optimierer nicht immer zusammenpassen. Wenn Sie wissen wollen, was im Code vor sich geht, sollten Sie sich wirklich die Disassemblierung ansehen. Eine gute IDE lässt Sie Schritt für Schritt durch die Assemblierung gehen und zB sehen, welche Register verglichen und welche Adressen geladen werden. Wenn der Optimierer eine Endlosschleife erzeugt hat, finden Sie eine Verzweigungsanweisung, die auf seine eigene Adresse zeigt.

Antworten (2)

( Site-übergreifendes Duplikat auf SO über den Thread-Fall und nicht über den Interrupt/Signal-Handler-Fall). Auch verwandt: Wann sollte man volatile mit Multi-Threading verwenden?


Ein Datenrennen auf einer Nicht- atomicVariablen 1 ist Undefined Behavior in C++11 2 . dh potentiell gleichzeitiges Lesen+Schreiben oder Schreiben+Schreiben ohne irgendeine Synchronisation, um eine Ereignis-bevor-Beziehung bereitzustellen, z. B. eine Mutex- oder Release/Acquire-Synchronisation.


Der Compiler darf davon ausgehen, dass sich choicezwischen zwei Lesevorgängen kein anderer Thread geändert hat (denn das wäre Data-Race UB ( Undefined Behavior )), sodass er CSE durchführen und den Check aus der Schleife ziehen kann.

Dies ist in der Tat das, was gcc tut (und die meisten anderen Compiler auch):

while(!choice){}

optimiert in asm, das so aussieht:

if(!choice)     // conditional branch outside the loop to skip it
    while(1){}  // infinite loop, like ARM  .L2: b .L2

Dies geschieht im zielunabhängigen Teil von gcc, gilt also für alle Architekturen.

Sie möchten , dass der Compiler diese Art der Optimierung durchführen kann, da echter Code Dinge wie for (int i=0 ; i < global_size ; i++ ) { ... }. Sie möchten, dass der Compiler in der Lage ist, das Global außerhalb der Schleife zu laden, und es nicht bei jedem Schleifendurchlauf oder für jeden späteren Zugriff in einer Funktion neu zu laden. Daten müssen sich in Registern befinden, damit die CPU damit arbeiten kann, nicht im Speicher.


Der Compiler könnte sogar davon ausgehen, dass der Code nie mit erreicht choice == 0wird, da eine Endlosschleife ohne Nebenwirkungen Undefined Behaviour ist. (Lesen / Schreiben von Nicht volatile-Variablen zählen nicht als Nebenwirkungen). Sachen wie printfsind ein Nebeneffekt, aber das Aufrufen einer Nicht-Inline-Funktion würde den Compiler auch daran hindern, das erneute Lesen von choicewegzuoptimieren, es sei denn, es wäre static int choice. (Dann würde der Compiler wissen, dass es nicht geändert werden printfkann, es sei denn, etwas in dieser Kompilierungseinheit wird &choicean eine Nicht-Inline-Funktion übergeben. Das heißt , die Escape-Analyse könnte es dem Compiler ermöglichen zu beweisen , dass static int choicees nicht durch einen Aufruf an eine "unbekannte" geändert werden kann Nicht-Inline-Funktion.)

In der Praxis optimieren echte Compiler einfache Endlosschleifen nicht weg, sie gehen davon aus (als Qualitätsproblem der Implementierung oder so), dass Sie schreiben wollten while(42){}. Aber ein Beispiel in https://en.cppreference.com/w/cpp/language/ub zeigt, dass Clang eine Endlosschleife wegoptimiert, wenn es Code ohne Nebeneffekte gab, den es wegoptimierte.


Offiziell unterstützte 100% portierbare / legale C++11-Möglichkeiten, dies zu tun:

Sie haben nicht wirklich mehrere Threads, Sie haben einen Interrupt-Handler. In C++11-Begriffen ist das genau wie ein Signalhandler: Er kann asynchron mit Ihrem Hauptprogramm laufen, aber auf demselben Kern.

C und C++ haben dafür schon lange eine Lösung: volatile sig_atomic_tEs ist garantiert in Ordnung, in einen Signalhandler zu schreiben und in Ihrem Hauptprogramm zu lesen

Ein ganzzahliger Typ, auf den als atomare Entität zugegriffen werden kann, selbst wenn asynchrone Interrupts durch Signale vorhanden sind.

void reader() {

    volatile sig_atomic_t shared_choice;
    auto handler = a lambda that sets shared_choice;

    ... register lambda as interrupt handler

    sig_atomic_t choice;        // non-volatile local to read it into
    while((choice=shared_choice) == 0){
        // if your CPU has any kind of power-saving instruction like x86 pause, do it here.
        // or a sleep-until-next-interrupt like x86 hlt
    }

    ... unregister it.

    switch(choice) {
        case 1: goto constant;
        ...
        case 0: // you could build the loop around this switch instead of a separate spinloop
                // but it doesn't matter much
    }
}

Andere Typen werden vom Standard nicht als atomar garantiert (obwohl sie in der Praxis auf normalen Architekturen wie x86volatile und ARM mindestens bis zu einer Zeigerbreite reichen , da Locals natürlich ausgerichtet werden. ist ein einzelnes Byte, und moderne ISAs können a atomar speichern Byte ohne Lesen/Ändern/Schreiben des umgebenden Wortes, trotz aller Fehlinformationen, die Sie vielleicht über wortorientierte CPUs gehört haben ).uint8_t

Was Sie wirklich möchten, ist eine Möglichkeit, einen bestimmten Zugriff flüchtig zu machen, anstatt eine separate Variable zu benötigen. Sie können das vielleicht mit tun *(volatile sig_atomic_t*)&choice, wie das ACCESS_ONCEMakro des Linux-Kernels, aber Linux wird mit deaktiviertem Strict-Aliasing kompiliert, um so etwas sicher zu machen. Ich denke, in der Praxis würde das auf gcc/clang funktionieren, aber ich denke, es ist kein streng legales C++.


Mit std::atomic<T>für lock-freeT

(mit std::memory_order_relaxedum eine effiziente asm ohne Barriereanweisungen zu erhalten, wie Sie von bekommen können volatile)

C++11 führt einen Standardmechanismus ein, um den Fall zu handhaben, in dem ein Thread eine Variable liest, während ein anderer Thread (oder Signalhandler) sie schreibt.

Es bietet Kontrolle über die Speicherreihenfolge, standardmäßig mit sequentieller Konsistenz, was teuer und für Ihren Fall nicht erforderlich ist. std::memory_order_relaxedAtomic Loads/Stores werden mit dem gleichen Asm (für Ihre K60 ARM Cortex-M4-CPU) wie kompiliert volatile uint8_t, mit dem Vorteil, dass Sie a uint8_tanstelle der Breite verwenden sig_atomic_tkönnen, während Sie dennoch sogar einen Hinweis auf C++11 Data Race UB vermeiden .

( Natürlich ist es nur auf Plattformen portierbar, auf denen atomic<T>Ihr T lock-frei ist; andernfalls kann der asynchrone Zugriff vom Hauptprogramm und ein Interrupt-Handler zu einem Deadlock führen . C++-Implementierungen dürfen keine Schreibvorgänge für umgebende Objekte erfinden , also wenn uint8_tüberhaupt , es sollte lock-free atomic sein.Oder verwenden Sie einfach unsigned char.Aber für Typen, die zu breit sind, um natürlich atomar zu sein, atomic<T>wird eine versteckte Sperre verwendet.Mit normalem Code ist es nicht möglich, jemals aufzuwachen und eine Sperre freizugeben, während der einzige CPU-Kern in einem steckt Interrupt-Handler, Sie sind am Arsch, wenn ein Signal/Interrupt ankommt, während diese Sperre gehalten wird.)

#include <atomic>
#include <stdint.h>

volatile uint8_t v;
std::atomic<uint8_t> a;

void a_reader() {
    while (a.load(std::memory_order_relaxed) == 0) {}
    // std::atomic_signal_fence(std::memory_order_acquire); // optional
}
void v_reader() {
    while (v == 0) {}
}

Beide kompilieren mit gcc7.2 -O3 für ARM im Godbolt-Compiler-Explorer auf dieselbe Anweisung

a_reader():
    ldr     r2, .L7      @ load the address of the global
.L2:                     @ do {
    ldrb    r3, [r2]        @ zero_extendqisi2
    cmp     r3, #0
    beq     .L2          @ }while(choice eq 0)
    bx      lr
.L7:
    .word   .LANCHOR0


void v_writer() {
    v = 1;
}

void a_writer() {
    // a = 1;  // seq_cst needs a DMB, or x86 xchg or mfence
    a.store(1, std::memory_order_relaxed);
}

ARM asm für beide:

    ldr     r3, .L15
    movs    r2, #1
    strb    r2, [r3, #1]
    bx      lr

In diesem Fall kann also für diese Implementierung volatiledasselbe wie std::atomic. Auf einigen Plattformen kann volatiledie Verwendung spezieller Anweisungen erforderlich sein, die für den Zugriff auf speicherabgebildete E/A-Register erforderlich sind. (Mir sind keine solchen Plattformen bekannt, und bei ARM ist dies nicht der Fall. Aber das ist eine Funktion, die volatileSie definitiv nicht wollen).


Mit atomickönnen Sie sogar die Neuordnung zur Kompilierzeit in Bezug auf nicht-atomare Variablen blockieren, ohne zusätzliche Laufzeitkosten, wenn Sie vorsichtig sind.

Verwenden Sie nicht .load(mo_acquire), das macht asm sicher in Bezug auf andere Threads, die gleichzeitig auf anderen Kernen laufen. Verwenden Sie stattdessen entspannte Loads/Stores und verwenden atomic_signal_fenceSie (nicht thread_fence) nach einem entspannten Laden oder vor einem entspannten Store , um eine Bestellung zu erwerben oder freizugeben.

Ein möglicher Anwendungsfall wäre ein Interrupt-Handler, der einen kleinen Puffer schreibt und dann ein atomisches Flag setzt, um anzuzeigen, dass er bereit ist. Oder ein atomarer Index, um anzugeben, welcher einer Gruppe von Puffern.

Beachten Sie, dass Sie, wenn der Interrupt-Handler erneut ausgeführt werden kann, während der Hauptcode noch den Puffer liest, ein Data Race UB haben (und einen tatsächlichen Fehler auf echter Hardware). UB (was der Compiler annehmen sollte, passiert nie).

Aber es ist nur UB, wenn es tatsächlich zur Laufzeit passiert; Wenn Ihr eingebettetes System über Echtzeitgarantien verfügt, können Sie möglicherweise garantieren, dass der Leser immer das Flag überprüfen und die nicht atomaren Daten lesen kann, bevor der Interrupt erneut ausgelöst werden kann, selbst im schlimmsten Fall, wenn ein anderer Interrupt eingeht und verzögert Dinge. Möglicherweise benötigen Sie eine Art Speicherbarriere, um sicherzustellen, dass der Compiler nicht optimiert, indem er weiterhin auf den Puffer verweist, anstatt auf das andere Objekt, in das Sie den Puffer einlesen. Der Compiler versteht nicht, dass zur Vermeidung von UB der Puffer sofort einmal gelesen werden muss, es sei denn, Sie sagen ihm das irgendwie. (Etwas wie GNU C asm("":::"memory")sollte es tun, oder sogar asm(""::"m"(shared_buffer[0]):"memory")).


Natürlich werden Lese-/Änderungs-/Schreiboperationen wie a++anders vonv++ , zu einem Thread-sicheren atomaren RMW kompiliert , indem eine LL/SC-Wiederholungsschleife oder ein x86 verwendet wird lock add [mem], 1. Die volatileVersion wird zu einem Ladevorgang kompiliert und dann zu einem separaten Speicher. Sie können dies mit Atomen ausdrücken wie:

uint8_t non_atomic_inc() {
    auto tmp = a.load(std::memory_order_relaxed);
    uint8_t old_val = tmp;
    tmp++;
    a.store(tmp, std::memory_order_relaxed);
    return old_val;
}

choiceWenn Sie tatsächlich jemals den Speicher erhöhen möchten , sollten Sie in Betracht ziehen volatile, Syntaxschmerzen zu vermeiden, wenn Sie dies anstelle von tatsächlichen atomaren Inkrementen möchten. Denken Sie jedoch daran, dass jeder Zugriff auf ein volatileoder atomicein zusätzliches Laden oder Speichern ist. Sie sollten also wirklich nur auswählen, wann Sie es in ein nicht atomares / nicht flüchtiges Lokal einlesen möchten.

Compiler optimieren derzeit keine atomaren , aber der Standard erlaubt es in Fällen, die sicher sind, es sei denn, Sie verwenden volatile atomic<uint8_t> choice.

Nochmals , was wir wirklich mögen, ist atomicder Zugriff, während der Interrupt-Handler registriert ist, dann der normale Zugriff.

C++20 stellt dies bereitstd::atomic_ref<>

Aber weder gcc noch clang unterstützen dies tatsächlich in ihrer Standardbibliothek (libstdc++ oder libc++). no member named 'atomic_ref' in namespace 'std', mit gcc und clang-std=gnu++2a . Es sollte jedoch kein Problem sein, es tatsächlich zu implementieren; GNU-C-Builtins __atomic_loadarbeiten gerne mit regulären Objekten, daher erfolgt die Atomizität eher pro Zugriff als pro Objekt.

void reader(){ 
    uint8_t choice;
    {  // limited scope for the atomic reference
       std::atomic_ref<uint8_t> atomic_choice(choice);
       auto choice_setter = [&atomic_choice] (int x) { atomic_choice = x; };

       ui::Context::addEventListener(ui::EventType::JOYSTICK_DOWN, &choice_setter);
       while(!atomic_choice) {}

       ui::Context::removeEventListener(ui::EventType::JOYSTICK_DOWN, &choice_setter);

    }

    switch(choice) { // then it's a normal non-atomic / non-volatile variable
    }
}

Sie landen wahrscheinlich mit einem zusätzlichen Laden der Variablen vs. while(!(choice = shared_choice)) ;, aber wenn Sie eine Funktion zwischen dem Spinloop und ihrer Verwendung aufrufen, ist es wahrscheinlich einfacher, den Compiler nicht zu zwingen, das letzte Leseergebnis in einem anderen lokalen (das es muss möglicherweise verschüttet werden). Oder ich denke, Sie könnten nach dem Deregistrieren ein Finale durchführen choice = shared_choice;, damit der Compiler choicenur ein Register behält und das Atomic oder Volatile erneut liest.


Fußnote 1:volatile

Sogar Datenrennen volatilesind technisch gesehen UB, aber in diesem Fall ist das Verhalten, das Sie in der Praxis bei echten Implementierungen erhalten, nützlich und normalerweise identisch atomicmit memory_order_relaxed, wenn Sie atomare Read-Modify-Write-Operationen vermeiden.

Wann sollte man volatile mit Multi-Threading verwenden? für den Multi-Core-Fall genauer erklärt: im Grunde nie, std::atomicstattdessen verwenden (mit entspannter memory_order).

Vom Compiler generierter Code, der geladen oder gespeichert uint8_twird, ist auf Ihrer ARM-CPU atomar. Lesen/Ändern/Schreiben wie choice++wäre kein atomares RMW auf volatile uint8_t choice, nur eine atomare Ladung, dann ein späterer atomarer Speicher, der auf andere atomare Speicher treten könnte.

Fußnote 2: C++03 :

Vor C++11 sagte der ISO-C++-Standard nichts über Threads aus, aber ältere Compiler funktionierten genauso; C ++ 11 hat im Grunde nur offiziell gemacht, dass die Art und Weise, wie Compiler bereits funktionieren, korrekt ist, indem die Als-ob-Regel angewendet wird, um das Verhalten eines einzelnen Threads nur beizubehalten, es sei denn, Sie verwenden spezielle Sprachfunktionen.

Betreff: "Volatile könnte bedeuten, dass spezielle Anweisungen verwendet werden, die für den Zugriff auf speicherabgebildete E / A-Register erforderlich sind." - Es funktioniert auf der Xtensa ISA von zB diesen ESP-8266-Chips: Die Dokumentation sagt, dass der Compiler die MEMWAnweisung ("Memory Wait") vor dem Lesen und nach dem Schreiben in flüchtige Variablen einfügen sollte, um sicherzustellen, dass die Daten durch alle / alle Pipelines oder Caches. IIRC, es gab auch einen bekannten Siliziumfehler, bei dem mehrere Schreibvorgänge auf denselben Speicherort in schneller Folge (ohne MEMW) dazu führen konnten, dass frühere Schreibvorgänge übersprungen wurden und nur die späteren Schreibvorgänge an Off-Core-Hardware/Speicher weitergegeben wurden.

Der Code-Optimierer hat den Code analysiert und nach dem, was er sehen kann, wird sich der Wert von choicenie ändern. Und da es sich nie ändern wird, macht es keinen Sinn, es überhaupt zu überprüfen.

Die Lösung besteht darin, die Variable volatileso zu deklarieren, dass der Compiler gezwungen ist, Code auszugeben, der ihren Wert unabhängig von der verwendeten Optimierungsstufe überprüft.

Ich bin einmal auf einen eingebetteten Compiler gestoßen, der Volatile ignorierte , als Sie den Optimierer einschalteten ... Oh, wie wir lachten, es dauerte Ewigkeiten, ihn zu finden, und wir würden uns durch die Assembly-Ausgabe kriechen. Alle Variablen, die außerhalb des normalen Kontrollflusses geändert werden, sollten als flüchtig deklariert werden, und ja, "volatile const" ist eine Sache!
In modernem C++ std::atomic<uint8_t> choicewäre es gut für die Kommunikation zwischen einem Interrupt-Handler und anderem Code. Verwenden Sie choice.store(value, std::memory_order_relaxed), und in dieser Schleife uint8_t tmp; while(0 == (tmp=choice.load(std::memory_order_relaxed)) {}wäre gut. (Und wahrscheinlich auf die gleiche Weise kompilieren wie volatile)
@PeterCordes: Die Verwendung std::atomic<uint8_t>wird sehr wahrscheinlich zu einer anderen Assembly führen als volatile(es sei denn, Sie verwenden diese seltsame MSVC-Erweiterung, die IIRC nur für x86, x64 und möglicherweise ARM funktioniert). Atomic muss den Wert atomar aktualisieren, dh kein Beobachter sollte in der Lage sein, einen Zwischenzustand zu sehen. OTOH volatilesagt nur "dieser Wert könnte sich seit dem letzten Lesen geändert haben", was viel weniger restriktiv ist. Außerdem gibt es auf einigen Plattformen spezielle Anweisungen für einige spezielle flüchtige Werte, zB speicherabgebildete Register.
@hoffmale: Du sprichst von atomaren RMW-Operationen wie .fetch_add(oder choice++). Ja, das wird natürlich anders kompiliert als beispielsweise volatilezu x86 lock add( Kann num++ für 'int num' atomar sein? ). Aber Pure-Load und Pure-Store sind bereits atomar für 8-Bit-Integer auf allen mir bekannten ISAs (außer dem frühen DEC Alpha, das nur Laden/Speichern in Wortgröße hat). (z. B. x86, Warum ist eine Integer-Zuweisung auf einer natürlich ausgerichteten Variablen auf x86 atomar? ).
@hoffmale: Nur zum Spaß habe ich mir die gcc-Ausgabe für x86 und MSP430 auf Godbolt angesehen, und das asm ist tatsächlich das gleiche für volatile oder atomare für den Writer und das Spin-Wait: godbolt.org/g/4MbgNN . Guter Punkt, dass volatile spezielle Anweisungen implizieren kann, das ist genau das, was Sie für die Kommunikation zwischen einem Interrupt-Handler und regulärem Code nichtatomic wollen, und ein guter Grund, . Wenn Sie jemals erhöhen möchten, können Sie es als schreiben tmp = choice; tmp++; choice=tmp;, was add byte [choice], 1auf x86 (ohne lockPräfix) kompiliert werden darf.
Ich bin neugierig, wie der Compiler den Code sieht. Die choiceVariable wurde innerhalb des Lambda referenziert , was eindeutig einen Nebeneffekt verursacht. Würde das den Compiler nicht dazu veranlassen, die Variable nicht wegzuoptimieren, da sich die Variable ändern könnte?
@DanielCheung: Das "Problem" mit dieser Referenz besteht darin, dass der Wert innerhalb der Schleife nicht geändert wird (dies geschieht gleichzeitig in einem Interrupt-Handler) - und es gibt keine Konstrukte (wie volatileoder Synchronisierungsprimitive), die andeuten, dass choicesie an anderer Stelle geändert werden könnten .
@DanielCheung: Ein Datenrennen auf einer Nicht- atomicVariablen ist Undefined Behaviour in C++11 (concurrent read+write), weshalb der Compiler in konvertieren darf while(!choice){}, if(!choice) infloop();dh die Last aus der Schleife heben darf. Viele Codes referenzieren wiederholt dieselbe globale Variable innerhalb einer Funktion, und C++11-Compiler dazu zu zwingen, sie zu deoptimieren, wäre ziemlich nervig.
@PeterCordes Eine unendliche Schleife ohne Nebenwirkungen ist undefiniertes Verhalten. en.cppreference.com/w/cpp/language/ub Als solches darf der Compiler weiter optimieren, wenn er möchte, und es einfach vollständig entfernen.
@PeterCordes Nein, das ist nicht richtig. Nichtatomarität ist nicht der Grund, warum der Compiler die Last aus der Schleife ziehen darf. Es ist viel einfacher, wie UKMonkey erklärt hat.
@iheanyi: UKMonkey hat Recht, Compiler dürfen das, aber das macht gcc in der Praxis nicht. GCC führt die von mir beschriebene Transformation durch: Konvertieren in Check Once, dann Endlosschleife. Data Race UB ermöglicht diese Transformation und würde sie auch dann noch ermöglichen, wenn volatileinnerhalb der Schleife ein Zugriff auf etwas anderes bestünde. (Oder so etwas wie printf, wenn es beweisen könnte, dass der Wert von . zB mit printfnicht geändert werden kann .) IMO data race UB ist hier wichtiger zu verstehen, weil es eine ganze Reihe von Optimierungen erklärt. choicestatic int choice
@iheanyi: gcc entscheidet sich dafür, Endlosschleifen wie while(42)oder nicht wegzuoptimieren while(u++ <= UINT_MAX). Einige Compiler optimieren basierend auf infloop UB (zumindest manchmal? Ich habe versucht, ein Beispiel zu erstellen, bin aber gescheitert: godbolt.org/g/KofYh6 ). Aber ich denke, als ich das letzte Mal gesehen habe, hat der Compiler (vielleicht MSVC?) Offensichtlich beabsichtigte Endlosschleifen beibehalten, als while(42){}ob andere Infloops verschwunden wären.
@hoffmale: Ich habe eine Antwort hinzugefügt, um all die Dinge zu klären, auf die mir die Leute in Kommentaren geantwortet haben.
Denken Sie noch etwas darüber nach ... Es gibt einige Optimierungsdurchläufe für Atomic ( x = 2; x = 3möglicherweise optimiert auf x = 3;, also kein Speicher für, 2da kein Beobachter sagen könnte, ob dieser Speicher stattgefunden hat oder nicht), die nicht für volatileWerte durchgeführt werden können (seit dem Lesen oder Das Schreiben eines volatileWerts kann bei einigen speziellen Werten, z. B. einem IO-Port, einen Nebeneffekt verursachen).
@UKMonkey "Das Lesen eines durch einen flüchtigen glvalue ([basic.lval]) bezeichneten Objekts, das Ändern eines Objekts, das Aufrufen einer Bibliotheks-E/A-Funktion oder das Aufrufen einer Funktion, die eine dieser Operationen ausführt, sind alle Nebeneffekte, die Änderungen in den Zustand der Ausführungsumgebung." Also nein, wenn Sie eine Endlosschleife haben, die auf eine nicht-atomare Flüchtigkeit wartet, ist es nicht konform, sie zu verwerfen.
@Alice, aber es ist nicht flüchtig.
@hoffmale: Nur zu Ihrer Information, ja, ISO C ++ auf Papier ermöglicht diese Art der Optimierung für Atomare. In der Praxis behandeln Compiler im Grunde alle Atomiken so ziemlich volatile atomicund optimieren sie nicht, da sie möglicherweise Probleme verursachen könnten. Siehe Warum führen Compiler redundante std::atomic-Schreibvorgänge nicht zusammen?
@DanMills - Ich habe gelacht, als ich zum ersten Mal darauf gestoßen volatile constbin, bis mir klar wurde, dass seine Verwendung tatsächlich Sinn macht - es ist ein Zeiger mit einer konstanten Adresse auf Daten, die flüchtig sind - ein E / A- Portvolatile const* .
@Den-Jason Es kann schlimmer werden, dass der Zielzeiger des DMA-Ping-Pong-Puffers beispielsweise in einem ISR aktualisiert wird, sowohl der Zeiger als auch die Daten flüchtig sind, oder ein Zeiger auf ein schreibgeschütztes Statusregister, volatile const * const uint32_t .. ..
@PeterCordes: Wenn die Autoren des Standards beabsichtigt hätten, dass eine Endlosschleife zu völlig willkürlichem Verhalten einlädt, hätten sie als Einschränkung angeben können, dass alle Schleifen entweder Nebenwirkungen haben oder beendet werden müssen. Die Vorstellung, dass „annehmen darf“ UB einlädt, wenn gegen eine Annahme verstoßen wird, steht im Gegensatz zur Verwendung des Begriffs „annehmen“ in anderen Bereichen menschlichen Strebens. Jemand, dem gesagt wird, dass er davon ausgehen könnte, dass eine Brücke repariert wird, bis er eine bestimmte Fahrt macht, sollte entschuldigt werden, wenn er zu spät kommt, wenn er an der Brücke ankommt und dann einen Umweg machen muss, aber nicht entschuldigt werden sollte ...
... für das Ignorieren von "BRIDGE OUT"-Zeichen auf der Grundlage, dass sie möglicherweise nicht korrekt sein könnten, wenn die Annahme nicht verletzt würde. Die "Als-ob"-Regel kann Situationen nicht wirklich berücksichtigen, in denen einige, aber nicht alle Verhaltensweisen, die mit der sequentiellen Ausführung nicht übereinstimmen, als akzeptabel angesehen werden sollten, aber meiner Meinung nach sollte der Standard es Compilern ermöglichen, die Ausführung (möglicherweise auf unbestimmte Zeit) zu verzögern einer Schleife über Code, der von dort aus statisch erreichbar ist und dessen einmal erreichte Ausführung durch nichts innerhalb der Schleife beeinflusst werden kann. Das würde viele Optimierungen ermöglichen...
... das würde negiert, wenn Endlosschleifen als UB behandelt würden (da die letztere Behandlung es für Programmierer erforderlich machen würde, Dummy-Nebeneffekte in Schleifen einzufügen, um zu verhindern, dass Compiler Code generieren, der sich auf inakzeptable Weise verhält).