Man kann Funktionen auch volatile deklarieren. Was passiert, wenn das
Hauptprogramm und die ISR gleichzeitig auf diese Funktion zugreifen?
Ich nehme an das volatile wird die ISR nicht hindern,
dazwischenzugrätschen.
z.B.:
uint8_t pinmode(uint8_t mode){
static uint8_t encuse=RTX;
switch(mode){
case ENC:encuse=ENC;RXI_D; break;
case RTX:encuse=RTX;EO_NPD;pbold|=(1<<RXPin);RXI_E; break;
case PRCL: break;
}
return encuse;
}
Statische Variablen in Funktionen sind böse, da diese die
Wiedereintrittsfähigkeit(Reentrance) vernichten.
Bernd K. schrieb:> Was passiert, wenn das> Hauptprogramm und die ISR gleichzeitig auf diese Funktion zugreifen?
Chaos.
Flickwerk:
Im Hauptprogramm die Interrupts VOR dem Aufruf verbieten.
Danach wieder erlauben
S. R. schrieb:> Ich sehe in deinem Beispielcode kein "volatile".
Ich auch nicht. Und auch nicht in der Dokumentation des gcc, wo es um
"function attributes" geht.
Was soll das denn sein?
Abgesehen davon geht es bei volatile Objekten keinesfalls um Locking,
sondern darum, die Zugriffe auf das RAM durch Nutzung von Registern
abzukürzen.
Ich empfehle einemal ein vollständiges, ohne Fehler und Warnungen
compilierbares, Codebeispiel zu posten.
Falls ich nämlich nicht (wieder mal) eine Neuerung in C verpasst habe,
dann gibt es keine "volatile Funktionen". Allenfalls solche die einen
volatile Rückgabewert haben.
In C++ gibt es in der Tat volatile Funktionen. Aber geht es hier um C
oder C++?
Deswegen die Frage nach einem Codebeispiel.
Bernd K. schrieb:> Man kann Funktionen auch volatile deklarieren.
Was soll das bewirken?
Hast Du mal ein Beispiel?
Bernd K. schrieb:> Ich nehme an das volatile wird die ISR nicht hindern,> dazwischenzugrätschen.
Richtig. Volatile schaltet nur die Optimierung einer Variablen aus. Mit
Interrupts hat das nichts zu tun.
Abgesehen davon geht es bei volatile Objekten keinesfalls um Locking,
sondern darum, die Zugriffe auf das RAM weniger zu optimieren.
Wenn du z.B. in einer Funktion eine globale (nicht volatile) Variable
mehrmals änderst, kann der Compiler das in CPU Registern machen und erst
ganz zum Schluss ins RAM zurück schreiben:
1
functionfoo()
2
{
3
bar++;
4
if(whatever)bar=bar+5
5
if(whatelse)bar=bar/2;
6
bar++;
7
}
Hier würden die Zwischenergebnis nur in einem Register stehen. Unter
Umständen geht der Compiler sogar noch einen Schritt weiter und hält die
Variable ausschließlich im Register, statt im RAM.
Wäre die Variable volatile, würde jede einzelne Zeile eine
read-modify-write Operation auf das ARM sein.
Bernd K. schrieb:> Man kann Funktionen auch volatile deklarieren.
In C eigentlich nicht. In C++ gibts das für member-Funktionen.
Allerdings haben volatile und ISRs in dem von dir genannten Kontext
nichts miteinander zu tun.
Oliver
Vielen Dank für die Antworten. Entschuldigung, dass ich erst jetzt
antworte. Die Notifikationen sind auf meiner Dienst-Email eingegangen.
In C gibt es also keine volatile Funktionen - hatte das offenbar in
einem C++ -Beispiel aufgeschnappt.
Vielen Dank auch an "ufuf" für den Hinweis, das statische Variablen die
Reentrance einer Funktion zerstören.
Mein Problem konnte ich durch atomare Programmierung cli/sei endlich
lösen.
(Zündzeitpunkts-Steuerung mit 3kHz-Differntial-Winkelencoder und
Soft-UART auf einem ATtiny85). Bitte melden falls einer am Code
interessiert ist.
In C++ tut volatile auch nicht das, was du davon erwarten würdest. Du
siehst das zu abstrakt - volatile ist, wie schon geschrieben wurde, nur
ein Hilfsmittel um dem Compiler einige Optimierungen beim Zugriff
abzugewöhnen, mehr nicht. Wenn man nicht weis, was der Compiler da tut,
hilft volatile einem auch nicht.
volatile gibt überhaupt gar keine Garantie zu irgendwas. Selbst Zugriffe
sind damit nicht automatisch atomar, d.h. wenn ISR und normaler Code
gleichzeitig auf eine volatile Variable zugreifen, kann dabei trotzdem
Datenmüll rauskommen. (Z.B. int auf AVR oder double auf ARM)
Streich volatile aus deinem Wortschatz und sag das am besten jedem
weiter, der es trotzdem benutzt.
Johannes schrieb:> Streich volatile aus deinem Wortschatz und sag das am besten jedem> weiter, der es trotzdem benutzt.
Das wäre allerdings wenig zielführend. Sinnvoller wäre:
Nutze volatile nur für genau das, für das es gedacht ist, und verstehe,
was es macht, und was nicht.
Ganz ohne geht es bei Memory-mapped Registern oder auch ISRs u.ä. halt
nicht.
Oliver
Johannes schrieb:> volatile gibt überhaupt gar keine Garantie zu irgendwas
Das ist falsch !
> Streich volatile aus deinem Wortschatz und sag das am besten jedem> weiter, der es trotzdem benutzt.
Und das ist gefährlicher Unsinn.
Johannes schrieb:> Streich volatile aus deinem Wortschatz und sag das am besten jedem> weiter, der es trotzdem benutzt.
Volatile ist durchaus wichtig.
Wenn du den Zweck nicht verstanden hast, dann ist das dein Problem.
War ja klar, dass jetzt wieder die Deppen kommen die Aussagen aus dem
Kontext zerren nur damit sie ihren Senf dazu abgeben können.
Für einen Anfänger ist volatile gefährlich. Es gibt mehr Leute die es
falsch benutzen als solche, die es richtig benutzen. Von daher ist für
einen Anfänger der Rat, darauf zu verzichten, sicherlich nicht falsch.
Wenn er mal in die Situation kommt sich mit write ordering oder
read/write barriers zumschlagen zu müssen, hat er auch verstanden was
volatile wirklich tut. Aber auch da gibt es bessere Lösungen als
volatile.
Und gerade im Kontext mit mehreren CPUs ist volatile dann plötzlich noch
viel gefährlicher als auf einem µC mit nur einer CPU.
Johannes schrieb:> dann plötzlich noch viel gefährlicher
Was für ein Schwachfug!
Nichts gefährliches ist da dran!
Einzig Notwendigkeiten hängen da dran.
Notwendigkeiten volatile einzusetzen.
Aus meiner Sicht:
Ein Leben ohne volatile ist nicht denkbar.
Zumindest wenn ISR und Multicore CPUs ins Spiel kommen
Und ja, dieser Thread behandelt ein ISR Problem.
Also ist volatile hier im Rennen!
Nicht so wie anfangs gedacht, aber im Rennen.
Normalerweise verwendet man auf multithreaded Systemen eine write
barrier, weil volatile halt einfach nicht sauber funktioniert. Aber ich
sag doch, dass die meisten volatile falsch einsetzen - und es noch nicht
einmal wissen.
Volatile hilft ein wenig bei single CPU wenn man write ordering möchte.
Wenn man dann halbwegs weis, welche Zugriffe vom Compiler so umgesetzt
werden das sie in einer Instruktion erledigt sind, kann man damit
funktionierende Synchronisationsmechanismen bauen.
Wenn man das aber nicht weis tut man Murks. Es kommt Code heraus der in
99% der Fälle sauber funktioniert, aber dann einmal in der Woche halt
doch nicht.
Bernd K. schrieb:> nehme an das volatile wird die ISR nicht hindern, dazwischenzugrätschen.
Das volatile ist sogar eher andersrum: du sagst damit dem Compiler, dass
du selber die Verantwortung für diese Variable übernehmen willst und er
gefälligst optimierenderweise seine Finger weg lassen soll.
Insofern kannst du dir da nur selber "reingrätschen".
Johannes schrieb:> Für einen Anfänger ist volatile gefährlich.
Ein volatile ist nur dann "gefährlich", wenn man meint, damit würde der
Compiler das schon richten. Und natürlich ist ein volatile zwingend
nötig, wenn eine (Haupt-)Schleife auf einen Interrupt reagieren soll.
Und der Interrupt sich nicht nur mit sich selber beschäftigt.
Johannes schrieb:> volatile gibt überhaupt gar keine Garantie zu irgendwas. Selbst Zugriffe> sind damit nicht automatisch atomar
Ein Semaphorenproblem samt inkonsistenter Daten hat eben nichts mit dem
Abschalten der Compileroptimierung durch ein volatile zu tun.
Die Semaphore garantiere ich, indem ich beim Manipulieren einer
Variablen dafür sorge, dass das zu diesem Zeitpunkt nur von einer Stelle
aus passiert und nicht eine ISR (oder gar eine weitere ISR) oder auch
ein anderer Prozessorkern gleichzeitig lesend oder schreibend darauf
zugreifen will.
Und das volatile sagt dem Compiler nur, dass sich die Variable jederzeit
"von aussen" ändern kann und sie deshalb nicht lokal optimiert werden
darf.
Das muss man erkannt und verstanden haben, dass da zwei völlig andere
Wirk- und Fehlermechnismen greifen.
Arduino Fanboy D. schrieb:> Zumindest wenn ISR und Multicore CPUs ins Spiel kommen
"volatile" ist in C und C++ zur Synchronisation auf Multicore-CPUs bzw.
in Multithreading-Umgebungen ungeeignet (in Java hingegen nicht, da
hat es aber eine andere Bedeutung!). In "normalem" C- oder C++-Code, der
auf einem Betriebssystem wie Linux läuft, ist "volatile" an Datentypen
immer falsch (im Sinne von: bringt nichts, macht wahrscheinlich nur
das Programm langsamer), außer bei "__asm__ volatile" aber das ist eine
andere Art von "volatile". Auf manchen CPU-Architekturen kann
"volatile" zur Multithreading-Synchronisation reichen, aber das ist dann
unportabel, und es gibt immer eine bessere, korrekte & portable Lösung.
Besonders hinterhältig ist, dass ein Multithreading-Programm mit
"volatile" den Anschein haben kann zu funktionieren (à la "funktioniert
in 99,999% der Aufrufe"), aber dennoch falsch ist und unter bestimmten
nicht kontrollierbaren Umständen dann doch versagt.
Für den Zugriff auf Memory-Mapped Register ist "volatile" meist
erforderlich. Für die Synchronisation zwischen ISRs&Main-Loop (auf der
selben CPU!) kann es sinnvoll sein, wenn nur einzelne Variablen
verändert werden, welche die CPU atomar bearbeiten kann. Oft sind aber
Interrupt-Sperren nötig.
Zur Vollständigkeit halber kann man C++-Member-Funktionen mit "volatile"
definieren (typischerweise in Libraries), wenn dieser aber außerhalb der
beiden genannten Use Cases (I/O und ISRs) aufgerufen werden, ist das
wieder falsch, wie im 1. Absatz gesagt.
Programmierer schrieb:> "volatile" ist in C und C++ zur Synchronisation auf Multicore-CPUs bzw.> in Multithreading-Umgebungen ungeeignet
Das hat der C- oder C++- Standard auch nie behauptet. Allerdings wird’s
halt aus Gewohnheit falsch verwendet.
C++ kippt daher ja auch das Kind mit dem Bade aus, siehe
Beitrag "avr-gcc-10 - "volatile deprecated [-Wvolatile]" Warnungen"
Oliver
Oliver S. schrieb:> Das hat der C- oder C++- Standard auch nie behauptet.
Eben. Das wissen nur viele nicht...
Oliver S. schrieb:> Allerdings wird’s> halt aus Gewohnheit falsch verwendet.
Ja. Und eben weil es manchmal so aussieht, als würde es funktionieren.
Ein kleines Beispiel:
Über "sleep" wird versucht, die Ausführung der "++counter" Zeile in den
einzelnen Threads nacheinander zu ordnen. Bei meinen Tests werden die
Zahlen 1-100 ausgegeben, sieht also so aus als wäre es korrekt. Ist aber
gleich in dreierlei Hinsicht falsch:
* Es gibt keine Garantie dass "sleep_for" eine Nacheinander-Ausführung
von "++counter" bewirkt.
* "++counter" ist nicht atomar; es kann sein dass mittendrin ein anderer
Thread diese Variable schreibt und das Ergebnis beliebig falsch ist.
* Die auf "counter" geschriebenen Werte müssen keineswegs in den anderen
Threads sichtbar sein. Es ist sehr wohl möglich, dass einfach 100x die
Zahl "1" ausgegeben wird, weil jeder Thread nur die initiale 0 sieht.
Eine Lösung ist die Verwendung von atomics:
Dies löst direkt die letzten beiden Probleme; das Addieren kann nicht
schief gehen, und das garantierte Memory-Ordering sorgt dafür, dass der
Ergebnis-Wert garantiert bei den anderen Threads ankommt. Die
Reihenfolge der Additionen ist immer noch nicht fix, was sich im
Ergebnis aber nicht niederschlägt, weil so garantiert 100x eine 1
addiert wird und so die Zahlen 1-100 ausgegeben werden (allerdings ggf.
in falscher Reihenfolge).
Oder man macht es ganz klassisch mit einem Mutex:
Löst das Problem ebenso (ggf. langsamer), denn Mutexe bewirken ebenfalls
eine garantierte Memory-Order, sodass die geschriebenen Werte bei den
anderen Threads sichtbar sind.
Beide Lösungen sind portabel und funktionieren immer (sofern ich nichts
falsch gemacht habe) und brauchen kein "volatile" und verlassen sich
somit nicht auf schmutzige Tricks.
Bernd K. schrieb:> Man kann Funktionen auch volatile deklarieren
Wenn du wirklich wissen willst, was das bedeutet, dann sieh dir
folgendes Video an: https://www.youtube.com/watch?v=KJW_DLaVXIY.
TL;DR: man kann an vielen Stellen 'volatile' verwenden (überall dort wo
man auch 'const' verwenden kann), d.h. aber noch lange nicht, dass es
eine Auswirkung hat und schon garnicht, dass es die Auswirkung hat, die
man sich erhofft.
Oliver S. schrieb:
> C++ kippt daher ja auch das Kind mit dem Bade aus, siehe
Eigenlich war das Ziel, dass volatile an den Stellen zu einer Warning
führt, wo es keinen Sinn macht.
Michael
Johannes schrieb:> Streich volatile aus deinem Wortschatz und sag das am besten jedem> weiter, der es trotzdem benutzt.
Ein volatile zuviel kann ein Programm etwas ineffizienter machen.
Ein volatile zuwenig kann es zum völligen Versagen bringen.
Was ist wohl besser?
Das Problem, dass manche nicht genau über die Wirkungsweise von volatile
Bescheid wissen, wird nicht dadurch behoben, dass man dieses Feature
ignoriert oder gar verbietet. Ganz im Gegenteil: Nur wer es benutzt,
wird sich ggf. auch mit seinen Details beschäftigen und damit etwas
dazulernen.
Yalu X. schrieb:> Ein volatile zuwenig kann es zum völligen Versagen bringen.>> Was ist wohl besser?
Zweiteres. Ein Programm welches nicht völlig versagt, sondern "meistens"
funktioniert, kann gefährlich werden; ein Programm welches überhaupt
nicht funktioniert wird nicht auf die Welt losgelassen.
Wenn man auf mehrere zusammengehörige "volatile" Variablen
hintereinander schreibt, und ein Interrupt genau dazwischen kommt, kann
die ISR inkonsistente Werte sehen. Ähnliche Probleme mit "int" oder
"float" auf AVR sind bekannt, wenn Variablen nur "halb" geschrieben
werden. Man sollte sich also sehr genau überlegen, ob "volatile" hier
hilft, oder nicht doch lieber Interrupt-Sperren. Rein zufällig enthält
das "cli()" Makro der avr-libc auch eine Optimizer-Barrier, sodass das
volatile hier sogar unnötig ist.
Programmierer schrieb:> Zweiteres. Ein Programm welches nicht völlig versagt, sondern "meistens"> funktioniert, kann gefährlich werden; ein Programm welches überhaupt> nicht funktioniert wird nicht auf die Welt losgelassen.
Mit dieser Begründung kann man ein typisches Mikrocontrollerprogramm
(mit memory-mapped I/O) nie auf die Welt loslassen, weil es ohne
volatile einfach nicht funktioniert.
> Wenn man auf mehrere zusammengehörige "volatile" Variablen> hintereinander schreibt, und ein Interrupt genau dazwischen kommt, kann> die ISR inkonsistente Werte sehen.
Das ist das bereits diskutierte Problem mit der Atomizität. Dieses
Problem wird aber nicht durch das Weglassen von volatile gelöst, sondern
durch die Verwendung geeigneter Konstrukte, die die Atomizität
gewährleisten.
Volatile braucht man, um z.B. auf Hardware mit Autoincrement
zuzugreifen. Man liest immer von der gleichen Adresse, aber kriegt
nacheinander die Daten. Z.B. CAN oder UART machen das so.
Für Interrupts benutzt man besser die <atomic.h>. Wie der Name schon
sagt, sorgt sie für den atomaren Zugriff. Daß Zugriffe nicht
wegoptimiert werden, ist dabei implizit.
Yalu X. schrieb:> Mit dieser Begründung kann man ein typisches Mikrocontrollerprogramm> (mit memory-mapped I/O) nie auf die Welt loslassen, weil es ohne> volatile einfach nicht funktioniert.
Die Begründung bezieht sich natürlich nur auf ISR-Synchronisation. Dass
es für Memory-Mapped-IO nötig ist schrieb ich ja bereits. Die
Alternative wäre hier übrigens Inline-Assembly, was aber eher hässlicher
ist.
Yalu X. schrieb:> Das ist das bereits diskutierte Problem mit der Atomizität. Dieses> Problem wird aber nicht durch das Weglassen von volatile gelöst, sondern> durch die Verwendung geeigneter Konstrukte, die die Atomizität> gewährleisten.
Diese Konstrukte wie eben cli()/sei() machen das volatile aber
überflüssig, weshalb man es hier von Anfang an ganz weglassen kann. Das
Suchen und Vermeiden von volatile in einer Codebasis ist hier also eine
sinnvolle Vorgehensweise.
Programmierer schrieb:> Yalu X. schrieb:>> Ein volatile zuwenig kann es zum völligen Versagen bringen.>>>> Was ist wohl besser?>> Zweiteres. Ein Programm welches nicht völlig versagt, sondern> "meistens" funktioniert, kann gefährlich werden
Schon recht. Nur war das überhaupt keine Option, die zur Wahl gestanden
hätte. Es ging um "etwas langsamer als nötig" vs. "funktioniert nicht".
> Wenn man auf mehrere zusammengehörige "volatile" Variablen> hintereinander schreibt, und ein Interrupt genau dazwischen kommt, kann> die ISR inkonsistente Werte sehen.
Diese Garantie gibt volatile ja auch nicht. Es ist vollkommen sinnlos,
sich (künstlich?) darüber aufzuregen, daß volatile nicht das tut, was du
dir wünschst, sondern nur das, was es garantiert.
Axel S. schrieb:> Schon recht. Nur war das überhaupt keine Option, die zur Wahl gestanden> hätte. Es ging um "etwas langsamer als nötig" vs. "funktioniert nicht".
Und die Abstufung "funktioniert meistens" gibt es nicht?
Axel S. schrieb:> Diese Garantie gibt volatile ja auch nicht. Es ist vollkommen sinnlos,> sich (künstlich?) darüber aufzuregen, daß volatile nicht das tut, was du> dir wünschst, sondern nur das, was es garantiert.
Niemand regt sich auf... Es geht nur darum, dass viele "volatile" falsch
verstehen, und annehmen, es würde so etwas garantieren. Daher kann man
nochmal klarstellen, was es nicht garantiert.
Programmierer schrieb:> Diese Konstrukte wie eben cli()/sei() machen das volatile aber> überflüssig, weshalb man es hier von Anfang an ganz weglassen kann. Das> Suchen und Vermeiden von volatile in einer Codebasis ist hier also eine> sinnvolle Vorgehensweise.
Das ist Unsinn. Weil eine Speicherstelle nicht nur durch einen Interrupt
sondern eben auch durch interne (z.B. Timer) oder externe (z.B IO)
Peripherie verändert werden kann. Und ohne volatile wird dann z.B. in
einer Schleife immer nur der Alte Wert gelesen und nicht der aktuelle.
Allein die ständige Reduktion hier von volatile auf die Synchronisation
ist der komplett falsche Ansatz.
Cyblord -. schrieb:> Weil eine Speicherstelle nicht nur durch einen Interrupt> sondern eben auch durch interne (z.B. Timer) oder externe (z.B IO)> Peripherie verändert werden kann.
Du redest also von Memory-Mapped IO? Dass man hier volatile braucht habe
ich von Anfang an gesagt:
Programmierer schrieb:> Für den Zugriff auf Memory-Mapped Register ist "volatile" meist> erforderlich.
Interessant wird es bei DMA. Dann braucht man aber sowieso ein
Konstrukt, um beim Lesen den Cache zu löschen ("invalidate") bzw. beim
Schreiben eben rauszuschreiben ("clean"), um nicht die "stale" Daten aus
dem Cache zu lesen bzw. um dem DMA-Agent die aktuellen Daten zu geben.
Dieses Konstrukt kann dann auch eine Optimization-Barrier beinhalten,
sodass "volatile" auch nicht mehr gebraucht wird. Wenn man für einen
Prozessor ohne (aktivierten) Cache oder mit Cache und DMA mit
Cache-Coherency ("Snooper") kompiliert, enthält das Konstrukt dann nur
noch die Barrier.
Cyblord -. schrieb:> Und ohne volatile wird dann z.B. in> einer Schleife immer nur der Alte Wert gelesen und nicht der aktuelle.
Nein, im Falle des cli()-Makros passiert das wegen der darin enthaltenen
memory-Barrier nicht. Ob es immer sinnvoll ist, bei jedem
Speicherzugriff die Interrupts komplett zu sperren, ist natürlich auch
fraglich. Man muß sich halt Gedanken machen.
Oliver
Cyblord -. schrieb:> Allein die ständige Reduktion hier von volatile auf die Synchronisation> ist der komplett falsche Ansatz.
Ja!
Volatile und Atomic, Mutex, Semaphor, sind unterschiedliche Instrumente,
welche erst im richtigen Zusammenspiel die angenehme/fehlerfreie Melodie
abliefern.
Davon abgesehen:
Mit jedem Sprachmittel kann man Mist bauen!
Das spricht nicht gegen das Sprachmittel.
(denn dann müsste man wohl alle abschaffen/verbieten)
Programmierer schrieb:> Du redest also von Memory-Mapped IO? Dass man hier volatile braucht habe> ich von Anfang an gesagt:
Nun deine, von mir zitierte, Aussage war recht allgemein gehalten.
Vielleicht hast du das nicht so gemeint, aber so geschrieben.
Aber auch im Zusammenspiel mit Interrupts und Atomic sieht doch der
Standardanwendungsfall für volatile so aus:
Es gibt einen Hauptschleife welche in jedem Durchgang auf einen
"Messwert" zugreift und damit was tut (z.B. Displayausgabe).
Dann gibt es einen Interrupt der diesen Messwert erzeugt.
Volatile ist notwendig da der Messwert sonst nie in der Hauptschleife
aktualisiert wird.
Atomic ist notwendig damit der Interrupt nicht mitten im Lesevorgang des
Messwerts anspringt und so die Hauptschleife teilweise alten Wert und
teilweise neuen Wert liest (bei Datentypen die nicht atomar kopiert
werden können).
Und ich sage mal, diese Anwendungsfälle treten häufig auf.
Damit ist deine Aussage widerlegt, man bräuchte mit Atomic kein
volatile.
> Nein, im Falle des cli()-Makros passiert das wegen der darin enthaltenen> memory-Barrier nicht.
cli ist ja nur das Implementierungsdetails eines Atomic Blocks. Es ist
schlechter Stil sich auf genau diese Implementierung und genau diesen
Seiteneffekt davon zu verlassen.
Man verwendet die atomic Makros ja damit man diese Details erst gar
nicht sieht.
Und auf der nächste Architektur ist atomic evt. ganz anders umgesetzt.
Nur mit volatile stellt man den gewünschten Effekt unabhängig davon
sicher.
Cyblord -. schrieb:> Es gibt einen Hauptschleife welche in jedem Durchgang auf einen> "Messwert" zugreift und damit was tut (z.B. Displayausgabe)
IMO ist das schlechtes Design, aber egal.
Cyblord -. schrieb:> Volatile ist notwendig da der Messwert sonst nie in der Hauptschleife> aktualisiert wird.
Wird er wohl. Zum Auslesen des Werts benutzt du eines der
avr-libc-"atomic" Makros wie ATOMIC_BLOCK (für andere Prozessoren kann
man sich leicht Äquivalente bauen). Dieses enthält eine Optimization
Barrier. Der Compiler ist damit gezwungen, den neuen Wert aus dem
Speicher zu laden.
Cyblord -. schrieb:> Damit ist deine Aussage widerlegt, man bräuchte mit Atomic kein> volatile.
Nö! std::atomic kann man zumindest auf Cortex-M so portieren dass es
auch reicht (mithilfe von LDREX/STREX). Die Optimization Barrier ist
dann in std::atomic mit Compiler-Builtins hartkodiert. Oder eben
ATOMIC_BLOCK, was auch eine Barrier enthält.
Cyblord -. schrieb:> Und auf der nächste Architektur ist atomic evt. ganz anders umgesetzt.
Die sind eh nicht standardisiert. Entweder man verwendet ein korrekt
portiertes std::atomic (braucht garantiert kein volatile) oder portiert
ATOMIC_BLOCK eben so, dass es die Optimization-Barrier enthält und kein
volatile braucht.
Programmierer schrieb:> Yalu X. schrieb:>> Ein volatile zuwenig kann es zum völligen Versagen bringen.>>>> Was ist wohl besser?>> Zweiteres. Ein Programm welches nicht völlig versagt, sondern "meistens"> funktioniert, kann gefährlich werden; ein Programm welches überhaupt> nicht funktioniert wird nicht auf die Welt losgelassen.
FALSCH! Klassischer Irrtum! Ein fehlendes volatile KANN es zum völligen
Versagen bringen, es tut es aber nicht IMMER! Und genau DAS ist das
Problem! Fehler, die nur sporadisch und unter sehr speziellen
Bedingungen auftreten! Viel Spaß beim Debugging!
Wenn ein Fehler, welcher Art auch immer, der ein Programm immer oder
sehr oft zum Versagen bringt, ist er harmlos, denn dann wird er schnell
gesehen und verhindert meist ein "ist jetzt zur Auslieferung fertig".
Falk B. schrieb:> Und genau DAS ist das Problem! Fehler, die nur sporadisch und unter sehr> speziellen Bedingungen auftreten! Viel Spaß beim Debugging!
Knick in der Optik? Genau das hab ich geschrieben. Unter "völlig
versagen" war wohl gemeint "stürzt sofort ab, lässt sich gar nicht erst
testen" und das Gegenteil wäre "scheint zu funktionieren aber man weiß
nicht genau ob es immer funktioniert". Und da ist natürlich ersteres
besser. Ein Programm mit viel "volatile" kann durchaus dieses Problem
haben, welches sich nicht mit noch mehr "volatile" beheben lässt.
Programmierer schrieb:> Axel S. schrieb:>> Diese Garantie gibt volatile ja auch nicht. Es ist vollkommen sinnlos,>> sich (künstlich?) darüber aufzuregen, daß volatile nicht das tut, was du>> dir wünschst, sondern nur das, was es garantiert.>> Niemand regt sich auf... Es geht nur darum, dass viele "volatile" falsch> verstehen, und annehmen, es würde so etwas garantieren.
Du scheinst das ja auch anzunehmen, obwohl du gleichzeitig weißt, dass
es nicht so ist. Du behauptest ja immerhin, volatile würde nicht "sauber
funktionieren".
Programmierer schrieb:> Diese Konstrukte wie eben cli()/sei() machen das volatile aber> überflüssig,
Nein. Beispiel: Eine Variable die von einer ISR verändert wird und im
Hauptprogramn benutzt wird (lesend oder schreibend, spielt keine Rolle).
Im Hauptprogramm würde die Variable evtl. für immer (in einem CPU
Register) gecached werden weil der Compiler davon ausgeht, daß die ISR
nie aufgerufen wird.
Um das zu verhindern, dafür ist volatile da. Es ersetzt kein locking, es
kann auch nicht durch locking ersetzt werden.
Wer mal (wie ich) zum ersten mal auf das Problem stößt, für den kann ein
Blick ins Assembler Listing sehr aufschlussreich sein.
Stefan ⛄ F. schrieb:> Programmierer schrieb:>> Diese Konstrukte wie eben cli()/sei() machen das volatile aber>> überflüssig,>> Nein.
Doch. So, wie die cli()/sei()-Makros für den AVR implementiert sind (und
um die ging es in dem Zusammenhang), schon.
Oliver
Stefan ⛄ F. schrieb:> Im Hauptprogramm würde die Variable evtl. für immer (in einem CPU> Register) gecached werden weil der Compiler davon ausgeht, daß die ISR> nie aufgerufen wird.
Nö. ATOMIC_BLOCK verhindert das. std::atomic auch, wenn korrekt
portiert. ATOMIC_BLOCK-Äquivalente für andere Prozessoren sollten es
auch verhindern; wenn nicht, baut man sich seine eigenen.
Aber ganz zu Anfang schrieb ich sogar, dass volatile hier nötig sein
kann:
Programmierer schrieb:> Für die Synchronisation zwischen ISRs&Main-Loop (auf der> selben CPU!) kann es sinnvoll sein, wenn nur einzelne Variablen> verändert werden, welche die CPU atomar bearbeiten kann. Oft sind aber> Interrupt-Sperren nötig.
Und die Interrupt-Sperre macht volatile eben überflüssig.
Rolf M. schrieb:> Du behauptest ja immerhin, volatile würde nicht "sauber> funktionieren".
Das tue ich nicht. Allerdings verlassen sich viele unsaubere Programme
auf Dinge, die volatile nicht garantiert. Einfach nur "volatile" an
möglichst viele Variablen tackern ist keine saubere Programmierung.
@Stefan ⛄ F.
Das stimmt halt nicht. Schau weiter oben, ein beliebtes Element zur
Synchronisierung ist z.B.
1
asm volatile("": : :"memory")
Dadurch schiebt der Compiler alle gecachten Werte vorher in die
entsprechenden Variablen und liest sie hinterher wieder neu ein.
Mir scheint diese Diskussion hier teilt sich in 2 Lager auf:
Die einen kennen Volatile, wissen was es tut und warum es mitunter sehr
böse sein kann.
Die anderen haben es zwar immer benutzt, aber nie wirklich verstanden
und sich auch nie nach Alternativen umgesehen. Diese Leute heulen jetzt
und verstehen die Welt nicht mehr: "Aber nimmt man doch immer...",
"Alternativlos..." usw.
Ein schönes Spiegelbild dafür, wie es in der Gesellschaft insgesamt
läuft.
Btw. wenn die exakte Zugriffssemantik auf ein Register oder ähnliches in
Hardware so wichtig ist, dann benutzt man halt einfach inline asm. Da
hat man volle Kontrolle was abläuft.
Das ist dann natürlich eine Methode und ein wenig weniger komfortabel
als einen volatile pointer zu dereferenzieren, aber C/C++ ist einfach
die falsche Spielwiese für solche Experimente.
Das sind produktive Programmiersprachen und Mikrocontroller sind nur ein
kleiner Teil vom Einsatzgebiet. Man möchte eine saubere Sprache bei der
klar ist wann was wie passiert, und volatile liegt da eindeutig in einer
Grauzone. Abgesehen von µC hat das einfach keinen Sinn.
Immerhin sorgt die drohende „Abkündigung“ von volatile in C++ dafür, daß
mancher sich seinen Code daraufhin mal anschaut, und erstaunliches
findet.
Z.B:
https://www.kdab.com/getting-rid-of-volatile-in-some-of-qt/
Das lässt für den Rest der weltweiten Codebasis Böses erahnen ;)
Oliver
Programmierer schrieb:> Cyblord -. schrieb:>> Und auf der nächste Architektur ist atomic evt. ganz anders umgesetzt.>> Die sind eh nicht standardisiert.
Das spielt keine Rolle. Die Zusicherung lautet das dieser Block atomar
ausgeführt wird. Das reicht.
Das Konstrukt ist dann austauschbar gegen jedes andere Konstrukt mit
dieser Zusicherung. Es müssen aber keine Annahmen über die Memory
Barrier gemacht werden.
Programmierer schrieb:> Das tue ich nicht. Allerdings verlassen sich viele unsaubere Programme> auf Dinge, die volatile nicht garantiert.
Gibts dafür ne Quelle oder ist das halt so eine Behauptung? Weil es
deine Meinung stützt dass jeder ausser dir leider ein bisschen doof ist?
Cyblord -. schrieb:> Das spielt keine Rolle. Die Zusicherung lautet das dieser Block atomar> ausgeführt wird.
Wer sichert das zu? Nur weil die avr-libc zufällig ein ATOMIC_BLOCK
Makro hat welches diese Funktion bietet ohne sie zu dokumentieren, muss
man jetzt annehmen dass man zwar immer so ein Makro zur Verfügung hat,
welches aber keine Barrier enthält? Wenn es die avr-libc gar nicht gäbe,
könnte man sich dann darauf verlassen dass sich immer so ein Makro
konstruieren lässt?
Verwende einfach statt ATOMIC_BLOCK cli/sei. Das hat die Barrier
garantiert & dokumentiert. Lässt sich natürlich ebenfalls mit Barrier
portieren.
Cyblord -. schrieb:> Gibts dafür ne Quelle oder ist das halt so eine Behauptung?
Wurde schon gepostet:
Oliver S. schrieb:> https://www.kdab.com/getting-rid-of-volatile-in-some-of-qt/>> Das lässt für den Rest der weltweiten Codebasis Böses erahnen ;)
Ganz genau. Wenn selbst Qt es nicht hinbekommt (bekam)...
Programmierer schrieb:> Wer sichert das zu?
Die API Beschreibung des Makros.
> Nur weil die avr-libc zufällig ein ATOMIC_BLOCK> Makro hat welches diese Funktion bietet ohne sie zu dokumentieren, muss> man jetzt annehmen dass man zwar immer so ein Makro zur Verfügung hat,> welches aber keine Barrier enthält?
Nein muss man nicht. Aber man kann jedes Konstrukt verwenden welches
diese Zusicherung macht. Was ist daran unverständlich für dich. Je
weniger Annahmen und Zusicherungen ich brauche, desto einfacher kann ich
Code portieren.
> Wenn es die avr-libc gar nicht gäbe,> könnte man sich dann darauf verlassen dass sich immer so ein Makro> konstruieren lässt?
Nein, wo habe ich das behauptet?
> Verwende einfach statt ATOMIC_BLOCK cli/sei. Das hat die Barrier> garantiert & dokumentiert. Lässt sich natürlich ebenfalls mit Barrier> portieren.
Kann man machen, nur wozu gibt es Abstraktion? Das Atomic kontrukt zeigt
dem Leser des Codes was beabsichtigt ist, und belästigt den Nutzer nicht
mit den Details seiner Implementierung.
Programmierer schrieb:> Rolf M. schrieb:>> Du behauptest ja immerhin, volatile würde nicht "sauber funktionieren".>> Das tue ich nicht.
Stimmt, war jemand anders. Sorry, hab ich verwechselt.
> Allerdings verlassen sich viele unsaubere Programme auf Dinge, die volatile> nicht garantiert. Einfach nur "volatile" an möglichst viele Variablen tackern> ist keine saubere Programmierung.
Klar. Da sind wir uns denke ich alle einig.
Johannes schrieb:> Die einen kennen Volatile, wissen was es tut und warum es mitunter sehr> böse sein kann.
…und wo es sinnvoll ist.
> Die anderen haben es zwar immer benutzt, aber nie wirklich verstanden> und sich auch nie nach Alternativen umgesehen.
Warum muss man sich denn nach Alternativen umsehen? Es tut das, was es
soll. Wenn man was anderes erwartet, wird man natürlich enttäuscht. Das
liegt dann aber eben daran, dass man es - wie du selbst sagst - nie
verstanden hat.
> Btw. wenn die exakte Zugriffssemantik auf ein Register oder ähnliches in> Hardware so wichtig ist, dann benutzt man halt einfach inline asm. Da> hat man volle Kontrolle was abläuft.
Du willst auf einem µC für jeden einzelnen Registerzugriff extra
inline-Assembler schreiben, nur um dir zu ersparen, die Register als
volatile zu definieren?
> Das ist dann natürlich eine Methode und ein wenig weniger komfortabel> als einen volatile pointer zu dereferenzieren, aber C/C++ ist einfach> die falsche Spielwiese für solche Experimente.
Eben. Deswegen lernt man, was volatile macht und was nicht und setzt
es dann entsprechend ein, statt einfach irgendwas zu erwarten und mal zu
probieren, ob das so funktioniert.
> Das sind produktive Programmiersprachen und Mikrocontroller sind nur ein> kleiner Teil vom Einsatzgebiet.
So klein dürfte der nicht sein.
Rolf M. schrieb:> So klein dürfte der nicht sein.
Mir kommt es so vor als hätte hier jemand was gegen C auf
Microcontrollern. Als wäre C nur was für Linux-Treiber und alle anderen
sollen gefälligst ASM verwenden auf ihren unpotenten Microcontrollern.
Cyblord -. schrieb:> Kann man machen, nur wozu gibt es Abstraktion? Das Atomic kontrukt zeigt> dem Leser des Codes was beabsichtigt ist, und belästigt den Nutzer nicht> mit den Details seiner Implementierung.
Bei der avr-libc ist es unglücklicherweise ein Implementierungs-Detail.
Andere Implementationen können (und sollten) explizit garantieren, dass
eine Optimization Barrier enthalten ist, genau wie andere
Synchronisations-Mechanismen es auch tun (std::atomic, std::mutex usw).
Ein Atomic-Block-Makro ohne Optimization-Barrier macht wenig Sinn (außer
man will eben unbedingt einen Grund haben die Barrier explizit mit
"volatile" zu erreichen), weshalb man sich nicht auf ein solches
einschränken muss. Im allerschlimmsten Fall bastelt man sich eben selbst
eines auf Basis der vorhandenen Möglichkeiten.
Programmierer schrieb:> Bei der avr-libc ist es unglücklicherweise ein Implementierungs-Detail.> Andere Implementationen können (und sollten) explizit garantieren, dass> eine Optimization Barrier enthalten ist
Aber garantiert hin oder her. Der Einsatz von volatile an dieser Stelle
ist trotzdem nicht falsch. Es ist zwar vielleicht nutzlos weil seine
Funktionalität schon vorweg genommen wurde, aber niemand setzt hier
falsche Erwartungen in volatile oder gefährdet damit die Funktionalität
des Programmes.
@Rolf M.
Sorry aber der Teil ist tatsächlich klein. Der Großteil der
Systembibliotheken auf Linux ist in C/C++ geschrieben, Linux Kernel,
Android Kernel, BSD, Gnome, GObject, KDE, Linpack, JVMs, etc. pp.
Auf Systemen mit Betriebssystem und mehreren Kernen (also alle ARM SoCs
der letzten Jahre) ist volatile auch gefährlich ohne Nutzen, da braucht
man richtige Synchronisationsmechanismen. Das betrifft alle Smartphones,
RPis usw.
Klar kann man mit volatile auch sinnvolle Sachen anstellen. Aber das Qt
Beispiel zeigt, dass die Gefahr einfach zu groß ist damit was falsch zu
machen. Andere Programmiersprachen haben wegen sowas keine sichtbaren
Zeiger. C/C++ sind zwar komplexer, sollten desswegen aber sicher nicht
einfach alles erlauben was geht.
Man muss auf dem uC auch sicher nicht nach jedem Registerzugriff ein
sync machen, oder? Reicht doch häufig nach einer Gruppe von Zugriffen.
Aber ich bin bei dir, das eine schönere Lösung hier wünschenswert wäre.
Volatile ist aber halt aus vielen Gründen keine gute Lösung.
> Warum muss man sich denn nach Alternativen umsehen? Es tut das, was es
soll.
Als Entwickler muss man sich immer auf den aktuellen Stand halten. Die
Welt dreht sich weiter. Nur weil Quecksilberschalter mal populär waren,
muss man die ja heute auch nicht mehr einsetzen.
Abgesehen davon tut es statistisch gesehen für die meisten nicht was es
soll, sie benutzen es einfach falsch.
Cyblord -. schrieb:> Der Einsatz von volatile an dieser Stelle> ist trotzdem nicht falsch.
Technisch nicht falsch. Aber sinnloser Code verwirrt den Leser, und das
ist mindestens genauso schlimm. Man kann auch überall im Code
Zeilenumbrüche oder nie benutzte Variablen einstreuen, aber das ist auch
keine besonders gute Idee.
Johannes schrieb:
Alles absolut richtig. Danke dafür!
Johannes schrieb:> Abgesehen davon tut es statistisch gesehen für die meisten nicht was es> soll, sie benutzen es einfach falsch.
Was du damit auch immer genau meinst. Bei AVRs wird volatile erstmal
hauptsächlich für Memory-Mapped Peripherie benutzt. Und da tut es genau
was es soll. Ein sehr geringer Teil der Programme verwendet volatile
überhaupt noch darüber hinaus.
Btw. ich bin tatsächlich kein Freund von C auf dem Mikrocontroller, ich
würde viel häufiger C++ verwenden ;-). Allein namespaces sind ein
ziemlich nützliches Feature, ebenso templates.
Programmierer schrieb:> Technisch nicht falsch. Aber sinnloser Code verwirrt den Leser
Sehe ich nicht so. Das volatile zeigt dem Leser genau was erwartet wird.
Und das stimmt dann auch.
Ohne volatile muss er erst auf den atomic block und dann auf das
dahinterliegende cli vordringen um zu verstehen warum das Programm
überhaupt funktioniert. D.h. er muss über C Kentnisse hinaus noch die
Implementierungsdetails der Architektur kennen.
Cyblord -. schrieb:> Sehe ich nicht so. Das volatile zeigt dem Leser genau was erwartet wird.> Und das stimmt dann auch.
Und der Leser weiß dass das "volatile" nicht etwa gebraucht wird, um
noch innerhalb des ATOMIC_BLOCKS irgendwie eine Schreib-Reihenfolge zu
garantieren, sondern wirklich nur weil man sich auf die
Standard-Bibliothek nicht verlassen kann?
Cyblord -. schrieb:> Ohne volatile muss er erst auf den atomic block und dann auf das> dahinterliegende cli vordringen um zu verstehen warum das Programm> überhaupt funktioniert. D.h. er muss über C Kentnisse hinaus noch die> Implementierungsdetails der Architektur kennen.
Oder einfach nur die Doku des Makros lesen. Wie gesagt ist es
unglücklich dass die avr-libc das nicht vernünftig dokumentiert. Für
Systeme mit korrekter Portierung von std::atomic kann man das einfach in
C++-Büchern nachlesen, dann ist es sogar portabel.
Programmierer schrieb:> unglücklich dass die avr-libc das nicht vernünftig dokumentiert. Für> Systeme mit korrekter Portierung von std::atomic
Die avrlibc dokumentiert das sehr wohl. Und das die als C-Library ein
std::atomic nicht korrekt portiert, ist ihr auch nicht vorzuwerfen ;)
Oliver
Imho besteht das Problem beim "Reinklatschen" von volatile darin, dass
man den Code überspezifiziert. Wie Programmierer schon angesprochen hat,
niemand weis hinterher welche der Sachen tatsächlich erforderlich sind
und welche nicht.
Wenn man solchen Code vorgesetzt bekommt und erweitern soll, oder halt
seinen eigenen Code nach 3 Jahren wieder verstehen will, ist das etwas
knifflig. Am Ende reißt man es ein und schreibt's neu, weil man sich nur
so sicher sein kann, das es tut, was es soll.
Man kann da ein wenig mit Kommentaren nachhelfen, aber spätestens da
würde ich mir dann doof vorkommen. Spätestens bei sowas wie
1
// NOTE: The following volatiles are not strictly required, but I'm not sure how this works and hopefully they make things a little bit more secure.
würde ich den Code löschen und sauber neu schreiben. Oder als Code
Reviewer den Change ablehnen und darauf bestehen, dass da Klarheit
geschaffen wird.
Programmierer schrieb:> Axel S. schrieb:>> Schon recht. Nur war das überhaupt keine Option, die zur Wahl gestanden>> hätte. Es ging um "etwas langsamer als nötig" vs. "funktioniert nicht".>> Und die Abstufung "funktioniert meistens" gibt es nicht?
Die gibt es. Aber sie ist in "funktioniert nicht" mit eingeschlossen.
Und nochmal: die Optionen waren "ein volatile zuviel" vs. "ein volatile
zu wenig". Letzteres ist das, was kaputt geht. Es gibt im Kontext dieser
Wahlmöglichkeiten genau gar keinen Grund, nochmal verschiedene Grade von
Kaputtheit zu unterscheiden.
> Es geht nur darum, dass viele "volatile" falsch> verstehen, und annehmen, es würde so etwas garantieren. Daher kann man> nochmal klarstellen, was es nicht garantiert.
Das ist aber etwas ganz anderes, als den Leuten zu sagen, sie sollten
volatile bitte ganz schnell vergessen.
Programmierer schrieb:> Oliver S. schrieb:>> Die avrlibc dokumentiert das sehr wohl.>> Oh, wo denn? Hier leider nicht:>> https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html#gaaaea265b31dabcfb3098bec7685c39e4>> Creates a block of code that is guaranteed to be executed atomically
Was fehlt dir denn da?
>> Und das die als C-Library ein>> std::atomic nicht korrekt portiert, ist ihr auch nicht vorzuwerfen ;)>> Logisch, std::atomic ist als Abkürzung für "std::atomic in C++ und> _Atomic in C" zu verstehen;
Sagt wer? Ich lese da "std::atomic" und das ist eindeutig C++
> implementiert die avr-libc letzteres?
Natürlich nicht. Das kann so nur der Compiler implementieren.
Axel S. schrieb:> Es gibt im Kontext dieser> Wahlmöglichkeiten genau gar keinen Grund, nochmal verschiedene Grade von> Kaputtheit zu unterscheiden.
Doch, die Kaputtheit die nur manchmal zuschlägt ist gefährlicher,
weshalb man diese separat betrachten kann.
Axel S. schrieb:> Das ist aber etwas ganz anderes, als den Leuten zu sagen, sie sollten> volatile bitte ganz schnell vergessen.
Das hab ich auch nicht.
Axel S. schrieb:> Was fehlt dir denn da?
Der Hinweis dass im Block auftretende Lese-Zugriffe nicht vor den Block
verschoben werden können, und dass im Block auftretende Schreibzugriffe
nicht nach den Block verschoben werden können ("optimization barrier").
Praktisch dass der Blockanfang Acquire-Semantik hat und das Block-Ende
Release-Semantik.
Axel S. schrieb:> Sagt wer?
Ich, denn ich war einfach nur zu faul es auszuführen. Die beiden
Mechanismen sind äquivalent, als Leser kann man diesen Zusammenhang also
erkennen.
Axel S. schrieb:> Natürlich nicht. Das kann so nur der Compiler implementieren.
Wenn, dann typischerweise aber in Zusammenarbeit mit der libc. Ich
vermute dass man es zumindest teilweise in C mit Inline-Asm hinbekommen
könnte. Es wird wahrscheinlich ohnehin darauf hinauslaufen, dass die
_Atomic-Funktionen einfach ATOMIC_BLOCK benutzen, da der AVR keine
Hardware-Atomics hat.
Programmierer schrieb:>> Es gibt im Kontext dieser Wahlmöglichkeiten genau gar>> keinen Grund, nochmal verschiedene Grade von>> Kaputtheit zu unterscheiden.>> Doch, die Kaputtheit die nur manchmal zuschlägt ist> gefährlicher, weshalb man diese separat betrachten kann.
Wenn ich die Wahl habe zwischen "ein volatile zu viel" und "ein volatile
zu wenig", dann ist die erste Wahl korrekt. Denn zuviel volatile macht
garantiert nichts kaputt, was nicht vorher schon kaputt war.
Dagegen führt ein benötigtes volatile weglassen zu einem kaputten
Programm.
> Axel S. schrieb:>> Das ist aber etwas ganz anderes, als den Leuten zu sagen,>> sie sollten volatile bitte ganz schnell vergessen.>> Das hab ich auch nicht.
Stimmt, das war der Johannes.
Dem du explizit zugestimmt hast.
Du bevorzugst ein garantiert kaputtes Programm (zuwenig volatile)
gegenüber einem zu langsamen, aber funktionierendem Programm (zuviel
volatile).
Johannes schrieb:> @Rolf M.>> Sorry aber der Teil ist tatsächlich klein. Der Großteil der> Systembibliotheken auf Linux ist in C/C++ geschrieben, Linux Kernel,> Android Kernel,
Der Android-Kernel ist der Linux-Kernel.
> BSD, Gnome, GObject, KDE, Linpack, JVMs, etc. pp.
Das ist der Teil, den du siehst. Aber es gibt auch extrem viel Code, der
nicht so offensichtlich ist, und davon läuft viel auf µCs, z.B. in
deiner Kaffeemaschine oder dem ESP deines Autos. Selbst am und im PC
wird es etliche µCs geben, z.B.im Controller deiner SSD oder dem
Tastaturcontroller, im Monitor und dem Drucker. Selbst so Dinge wie die
Ansteuerung der bunten Show-LEDs in der Grafikkarte wird einen eigenen
µC haben. In einem Handy wird es auch jede Menge Mikrocontroller geben.
Und das wird überwiegend C-Code sein.
µCs kommen halt in etwas mehr Komponenten vor als nur der
selbstgebastelten Propeller-Clock.
> Man muss auf dem uC auch sicher nicht nach jedem Registerzugriff ein> sync machen, oder? Reicht doch häufig nach einer Gruppe von Zugriffen.
Kommt immer ganz drauf an, würde ich sagen. Wesentlich ist auf jeden
Fall, dass mehrere aufeinanderfolgende Zugriffe auf das selbe Register
auch wirklich alle stattfinden und der Compiler nicht alle bis auf einen
wegoptimiert, wie er es bei einer regulären Variable tun würde.
> Aber ich bin bei dir, das eine schönere Lösung hier wünschenswert wäre.> Volatile ist aber halt aus vielen Gründen keine gute Lösung.>>> Warum muss man sich denn nach Alternativen umsehen? Es tut das, was es> soll.>> Als Entwickler muss man sich immer auf den aktuellen Stand halten. Die> Welt dreht sich weiter. Nur weil Quecksilberschalter mal populär waren,> muss man die ja heute auch nicht mehr einsetzen.
Wenn es was überzeugendes gibt, das alle_ Anwendungsfälle auf _allen
Plattformen abdeckt, das gleichzeitig einen erkennbaren Vorteil bietet,
dann bin ich auch dafür. Nur sehe ich das im Moment nicht so wirklich.
> Abgesehen davon tut es statistisch gesehen für die meisten nicht was es> soll, sie benutzen es einfach falsch.
Das ist jetzt Ansichtssache, was du unter "soll" verstehst. Für mich
heißt es, dass es sich entsprechend der Spezifikation verhält und nicht
unbedingt so, wie sich das der Anwender ggf. wünschen würde.
Rolf M. schrieb:> Der Android-Kernel ist der Linux-Kernel.
Nein. Der Android-Kernel ist ein aktuell gehaltener, Google-eigener Fork
des Linux-Kernels. Man kann kein CDD-konformes Android mit einem
Mainline-Linux bauen.
Arduino Fanboy D. schrieb:> Statische Variablen in Funktionen sind böse, da diese die> Wiedereintrittsfähigkeit(Reentrance) vernichten.
Kannst Du das ein bisschen näher erläutern? Würde mich mal
interessieren.
In einer AVR Appnote steht, dass man static-Variablen innerhalb von
Funktionen vermeiden soll, da für diese
1) Speicher an ungünstigen Stellen bereitgestellt werden muss und
2) Zusätzlicher Code zum Lesen und Schreiben dieser Variablen erzeugt
wird, was wiederum zusätzliche Zyklen erfordert.
Soweit verständlich.
Aber was hat es mit dem Wiedereintritt auf sich?
Programmierer schrieb:> Axel S. schrieb:>> Was fehlt dir denn da?>> Der Hinweis dass im Block auftretende Lese-Zugriffe nicht vor den Block> verschoben werden können, und dass im Block auftretende Schreibzugriffe> nicht nach den Block verschoben werden können ("optimization barrier").
Und der Teil, der garantiert, dass die Zugriffe, die innerhalb des
Blocks stehen, überhaupt alle wirklich stattfinden. Gerade das ist ja
die Hauptaufgabe von volatile.
> Axel S. schrieb:>> Natürlich nicht. Das kann so nur der Compiler implementieren.>> Wenn, dann typischerweise aber in Zusammenarbeit mit der libc.
Die eigentliche Funktionalität muss dabei aber im Compiler stecken.
> Ich vermute dass man es zumindest teilweise in C mit Inline-Asm hinbekommen> könnte.
Wie soll das gehen? Der Code innerhalb des Blocks wird vom Compiler
generiert, und der muss diesen dann anders generieren.
eine Funktion in einer ISR und gleichzeitig auch außerhalb einer ISR
aufgerufen werden könnte. Oder wenn die Funktion sich selbst rekursiv
aufruft.
S. R. schrieb:> Wenn ich die Wahl habe zwischen "ein volatile zu viel" und "ein volatile> zu wenig", dann ist die erste Wahl korrekt.
Zum Glück hat man diese Wahl nie.
S. R. schrieb:> Dagegen führt ein benötigtes volatile weglassen zu einem kaputten> Programm.
Wenn man std::atomic nutzt, hat man dieses Problem gar nicht erst.
S. R. schrieb:> Du bevorzugst ein garantiert kaputtes Programm (zuwenig volatile)> gegenüber einem zu langsamen, aber funktionierendem Programm (zuviel> volatile).
Nein. Meine Präferenz-Reihenfolge:
* Ein Programm mit std::atomic, std::mutex & Co (funktioniert
garantiert) ganz ohne volatile
* Ein Programm mit viel zu wenig Synchronisation, welches so kaputt ist
dass es sich gar nicht testen lässt, weil es dann nie released wird und
keinen Schaden anrichten kann (nutzlos)
* Ein Programm mit exakt der richtigen Menge an "volatile" (schwierig zu
erstellen und warten)
* Ein Programm mit etwas zu wenig Synchronisation, welches in manchmal
nicht funktioniert (gefährlich).
Rolf M. schrieb:> Und der Teil, der garantiert, dass die Zugriffe, die innerhalb des> Blocks stehen, überhaupt alle wirklich stattfinden.
Sie finden ggf. nicht einzeln statt. Wenn man mehrere, einzelne Zugriffe
braucht, braucht man mehrere Atomic-Blöcke, und daraus werden dann
einzelne Zugriffe kompiliert.
Rolf M. schrieb:> Wie soll das gehen? Der Code innerhalb des Blocks wird vom Compiler> generiert, und der muss diesen dann anders generieren.
Hä? Reden wir von der selben Sache? Eine Atomic-Addition könnte bei AVR
so aussehen:
Programmierer schrieb:> Rolf M. schrieb:>> Und der Teil, der garantiert, dass die Zugriffe, die innerhalb des>> Blocks stehen, überhaupt alle wirklich stattfinden.>> Sie finden ggf. nicht einzeln statt.
Was meinst du damit? Mir geht es konkret um sowas wie z.B. eine
Schleife, in der so eine atomare Operation durchgeführt wird. Der
Optimizer könnte auf die Idee kommen, diese wiederholten Zugriffe auf
den RAM zu entfernen und den Wert nur in einem Register zu halten. Bei
der Dokumentation von ATOMIC_BLOCK sehe ich keine Garantie, dass sowas
nicht passiert.
> Rolf M. schrieb:>> Wie soll das gehen? Der Code innerhalb des Blocks wird vom Compiler>> generiert, und der muss diesen dann anders generieren.>> Hä? Reden wir von der selben Sache?
Vielleicht nicht.
> Eine Atomic-Addition könnte bei AVR so aussehen:> int atomic_fetch_add_int (int* obj, int arg) {> int res;> ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {> res = *obj;> obj += arg;> }> return res;> }> Braucht nichtmal Assembler.
Ich dachte, es geht um die Umsetzung des Makros ATOMIC_BLOCK so, dass es
volatile überflüssig macht, und das ohne spezielle Unterstützung seitens
des Compilers.
Programmierer schrieb:>> Dagegen führt ein benötigtes volatile weglassen>> zu einem kaputten Programm.>> Wenn man std::atomic nutzt, hat man dieses Problem gar nicht erst.
Aha. Du hast gerade bewiesen, dass du den Unterschied zwischen
std::atomic und volatile nicht kennst. Die tun nämlich nicht dasselbe
und ersetzen einander nicht.
Super.
Rolf M. schrieb:> Der Optimizer könnte auf die Idee kommen, diese wiederholten Zugriffe> auf den RAM zu entfernen und den Wert nur in einem Register zu halten.
Macht er aber nicht, wenn der Zugriff einzeln im ATOMIC_BLOCK steht und
nicht die ganze Schleife. Das haben wir doch jetzt schon 25x diskutiert.
Rolf M. schrieb:> Bei der Dokumentation von ATOMIC_BLOCK sehe ich keine Garantie, dass> sowas nicht passiert
Darum geht es doch die ganze Zeit ?♂️ es ist aber so, da das sei()
innerhalb des Makros einen Memory Clobber enthält. Das fehlt lediglich
in der Doku.
Rolf M. schrieb:> Ich dachte, es geht um die Umsetzung des Makros ATOMIC_BLOCK so, dass es> volatile überflüssig macht,
Nein, ein solches Makro macht keinen Sinn. Ich rede von einer
Implementation von std::atomic bzw _Atomic.
S. R. schrieb:> Die tun nämlich nicht_ dasselbe und ersetzen einander _nicht.
Genau das sagte ich in meinem 1. Post. Super.
Auf Standard konformen "hosted" Implementationen von C und C++ ersetzt
atomic sehr wohl volatile. Für ISR Synchro sollte es auch weitgehend
volatile ersetzen. Für Memory Mapped IO nicht.
S. R. schrieb:> Die tun nämlich nicht dasselbe> und ersetzen einander nicht.
Ja sie tun nicht das selbe. Aber atomics ersetzt nahezu alle volatiles,
wo diese fälschlicherweise eingesetzt werden/wurden ;-)
Das folgende Beispiel demonstriert, wozu volatile gut ist und dass man
volatile nicht immer durch einen atomic-block ersetzen kann:
1
#include<stdint.h>
2
#include<avr/io.h>
3
#include<avr/interrupt.h>
4
5
uint8_ti=255;
6
7
ISR(PCINT0_vect)
8
{
9
i=255;
10
}
11
12
intmain(void)
13
{
14
// Port C = Ausgang
15
PORTC=255;
16
17
// Interrupt bei Signal von PB0
18
PCICR=(1<<PCIE0);
19
PCMSK0=(1<<PCINT0);
20
sei();
21
22
do
23
{
24
i=i-1;
25
PINB;// damit die Schleife nicht wegoptimiert wird
26
}
27
while(i>0);
28
29
// Ergebnis ausgeben
30
PORTC=i;
31
}
Das Programm soll die Variable i von 255 auf 0 herunter zählen und dann
auf Port C ausgeben. Wenn aber zwischendurch
der Interrupt ausgelöst wird, soll sie zurück auf 255 gesetzt werden.
Ein Blick in den Assembler-Code zeigt, dass dieses Programm nicht
funktioniert:
1
000000b4 <main>:
2
b4: 8f ef ldi r24, 0xFF
3
b6: 88 b9 out 0x08, r24
4
b8: 81 e0 ldi r24, 0x01
5
ba: 80 93 68 00 sts 0x0068, r24
6
be: 80 93 6b 00 sts 0x006B, r24
7
c2: 78 94 sei
8
c4: 80 91 00 01 lds r24, 0x0100 ; R24 = i
9
10
c8: 81 50 subi r24, 0x01 ; subtrahiere 1 von R24
11
ca: 80 93 00 01 sts 0x0100, r24 ; i = R24
12
ce: 93 b1 in r25, 0x03 ; PINB lesen
13
d0: 81 11 cpse r24, r1 ; ist R24 = 0?
14
d2: fa cf rjmp .-12 ; Rücksprung nach 0xc8
15
16
d4: 18 b8 out 0x08, r1 ; Gib 0 auf PORTC aus
17
d6: 80 e0 ldi r24, 0x00
18
d8: 90 e0 ldi r25, 0x00
19
da: 08 95 ret
Der Fehler entsteht durch den Rücksprung nach 0xc8. Hier wird R24
wiederholt verringert ohne die RAM Zelle i erneut zu lesen. Unterhalb
der while Schleife wird dann einfach fest 0 auf PORTC ausgegeben (r1 ist
immer 0). Was die ISR macht, wird einfach völlig ignoriert. Das
Schlüsselwort volatile (vor uint8_t i=255;) behebt diesen Fehler:
1
000000b4 <main>:
2
b4: 8f ef ldi r24, 0xFF
3
b6: 88 b9 out 0x08, r24
4
b8: 81 e0 ldi r24, 0x01
5
ba: 80 93 68 00 sts 0x0068, r24
6
be: 80 93 6b 00 sts 0x006B, r24
7
c2: 78 94 sei
8
9
c4: 80 91 00 01 lds r24, 0x0100 ; R24 = i
10
c8: 81 50 subi r24, 0x01 ; subtrahiere 1 von R24
11
ca: 80 93 00 01 sts 0x0100, r24 ; i = R24
12
ce: 83 b1 in r24, 0x03 ; PINB lesen
13
d0: 80 91 00 01 lds r24, 0x0100 ; R24 = i
14
d4: 81 11 cpse r24, r1 ; ist R24 = 0?
15
d6: f6 cf rjmp .-20 ; Rücksprung nach 0xc4
16
17
d8: 80 91 00 01 lds r24, 0x0100 ; R24 = i
18
dc: 88 b9 out 0x08, r24 ; Gib R24 auf PORTC aus
19
de: 80 e0 ldi r24, 0x00
20
e0: 90 e0 ldi r25, 0x00
21
e2: 08 95 ret
Dieses mal wird die Variable bei jedem Zugriff erneut aus dem RAM
gelesen, so dass die Änderung durch die ISR wirksam ist.
Mit einem Atmoc-Block kann man das nicht lösen:
1
#include<stdint.h>
2
#include<avr/io.h>
3
#include<avr/interrupt.h>
4
#include<util/atomic.h>
5
6
uint8_ti=255;
7
8
ISR(PCINT0_vect)
9
{
10
i=255;
11
}
12
13
intmain(void)
14
{
15
// Port C = Ausgang
16
PORTC=255;
17
18
// Interrupt bei Signal von PB0
19
PCICR=(1<<PCIE0);
20
PCMSK0=(1<<PCINT0);
21
sei();
22
23
do
24
{
25
ATOMIC_BLOCK(ATOMIC_FORCEON)
26
{
27
i=i-1;
28
}
29
PINB;// damit die Schleife nicht wegoptimiert wird
30
}
31
while(i>0);
32
33
// Ergebnis ausgeben
34
PORTC=i;
35
}
Jetzt wird zwar innerhalb der Schleife i immer schön neu aus dem RAM
gelesen, aber zum Schluss wird doch wieder einfach 0 auf PORTC
ausgegeben:
1
000000b4 <main>:
2
b4: 8f ef ldi r24, 0xFF
3
b6: 88 b9 out 0x08, r24
4
b8: 81 e0 ldi r24, 0x01
5
ba: 80 93 68 00 sts 0x0068, r24
6
be: 80 93 6b 00 sts 0x006B, r24
7
c2: 78 94 sei
8
9
c4: f8 94 cli
10
c6: 80 91 00 01 lds r24, 0x0100 ; R24 = i
11
ca: 81 50 subi r24, 0x01 ; subtrahiere 1 von R24
12
cc: 80 93 00 01 sts 0x0100, r24 ; i = R24
13
d0: 78 94 sei
14
d2: 83 b1 in r24, 0x03 ; PINB lesen
15
d4: 80 91 00 01 lds r24, 0x0100 ; R24 = i
16
d8: 81 11 cpse r24, r1 ; ist R24 = 0?
17
da: f4 cf rjmp .-24 ; Rücksprung nach 0xc4
18
19
dc: 18 b8 out 0x08, r1 ; Gib 0 auf PORTC aus
20
de: 80 e0 ldi r24, 0x00
21
e0: 90 e0 ldi r25, 0x00
22
e2: 08 95 ret
Man könnte nun versucht sein, den Automic-Block zu vergrößern:
1
#include<stdint.h>
2
#include<avr/io.h>
3
#include<avr/interrupt.h>
4
#include<util/atomic.h>
5
6
uint8_ti=255;
7
8
ISR(PCINT0_vect)
9
{
10
i=255;
11
}
12
13
intmain(void)
14
{
15
// Port C = Ausgang
16
PORTC=255;
17
18
// Interrupt bei Signal von PB0
19
PCICR=(1<<PCIE0);
20
PCMSK0=(1<<PCINT0);
21
sei();
22
23
ATOMIC_BLOCK(ATOMIC_FORCEON)
24
{
25
do
26
{
27
i=i-1;
28
PINB;// damit die Schleife nicht wegoptimiert wird
29
}
30
while(i>0);
31
32
// Ergebnis ausgeben
33
PORTC=i;
34
}
35
}
Aber auch das generiert fehlerhaften Code:
1
000000b4 <main>:
2
b4: 8f ef ldi r24, 0xFF
3
b6: 88 b9 out 0x08, r24
4
b8: 81 e0 ldi r24, 0x01
5
ba: 80 93 68 00 sts 0x0068, r24
6
be: 80 93 6b 00 sts 0x006B, r24
7
c2: 78 94 sei
8
c4: f8 94 cli
9
c6: 80 91 00 01 lds r24, 0x0100 ; R24 = i
10
11
ca: 81 50 subi r24, 0x01 ; subtrahiere 1 von R24
12
cc: 80 93 00 01 sts 0x0100, r24 ; i = R24
13
d0: 93 b1 in r25, 0x03 ; PINB lesen
14
d2: 81 11 cpse r24, r1 ; ist R24 = 0?
15
d4: fa cf rjmp .-12 ; Rücksprung nach 0xca
16
17
d6: 18 b8 out 0x08, r1 ; Gib 0 auf PORTC aus
18
d8: 78 94 sei
19
da: 80 e0 ldi r24, 0x00
20
dc: 90 e0 ldi r25, 0x00
21
de: 08 95 ret
Die Schleife kann nicht mehr durch die ISR unterbrochen werden. Folglich
verkürzt der Compiler die Ausgabe auf PORTC wieder auf den festen Wert
0. Andere Werte können gar nicht mehr vorkommen.
Ich sehe hier keine Chance, dieses kleine Programm ohne volatile ans
Laufen zu bekommen.
Ich habe mir für die Mikrocontroller-Programmierung folgende Regeln
gemerkt:
1) Wenn eine Variable in eine ISR beschrieben und außerhalb gelesen wird
(oder umgekehrt), muss sie volatile sein.
2) Wenn die Variable größer ist, als die CPU in einem Rutsch
lesen/schreiben kann, dann braucht man außerdem noch einen atomic-block
(oder cli/sei). Ansonsten kann man beim Lesen inkonsistente Daten
bekommen, weil nur eine Hälfte aktualisiert wurde. Bei AVR betrifft das
alle Variablen, die größer als 8bit sind.
... ist so natürlich ineffizienter. Echtes std::atomic erlaubt über die
Memory Order Parameter präzisere Angaben darüber, wann welche
Synchronisation erforderlich ist, um unnötige Operationen zu vermeiden.
ATOMIC_BLOCK ist hier eher Holzhammer.
Na ja, in dem Fall würden zwei Atomic-Blöcke, oder eine Memory Barrier
vor der letzten Zeile per cli(), asm volatile("": : :"memory"), oder
__sync_synchronize das Problem lösen, auch ohne volatile.
Oliver
Programmierer schrieb:> ATOMIC_BLOCK ist hier eher Holzhammer.
Sieht man.
Ich habe das Gefühl, dass hier jetzt das atomic-block nur noch wegen
seinem Seiteneffekt genutzt wird. Außerdem haben wir jetzt zu 50% der
Schleifen-Zeit alle Interrupts gesperrt, was gar nicht nötig wäre.
Die primäre Funktion des atomic-block ist laut der Doku offenbar:
> The term "Atomic" in this context refers to the unability> of the respective code to be interrupted."
Ich will ja gar nicht verbieten, das Interrupts dazwischen kommen. Was
aber nicht geht ist, dass die Änderungen an i durch die ISR komplett
ignoriert werden.
In den weiteren Erklärungen zeigen sie, wann und warum man atomic-block
mit volatile kombinieren soll.
Quelle:
https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html
Ich würde mich lieber daran halten, um unangenehme Überraschungen nach
Compiler-Upgrade zu vermeiden.
Nein du kehrst die Auswirkungen von volatile unter den Teppic.
volatile sorgt bei jedem *einzelnen* Zugriff dafür, dass der Wert neu
gelesen wird. Geht mit atomic einwandfrei nachzubilden, muss dann aber
bei jedem *einzelnen* Zugriff auch angegeben werden - zumindest mit
dem avr libc Konstrukt.
Programmierer schrieb:> Auf Standard konformen "hosted" Implementationen von> C und C++ ersetzt atomic sehr wohl volatile.> Für ISR Synchro sollte es auch weitgehend volatile ersetzen.
Das ist falsch.
> Für Memory Mapped IO nicht.
Das ist richtig.
mh schrieb:> Aber atomics ersetzt nahezu alle volatiles,> wo diese fälschlicherweise eingesetzt werden/wurden ;-)
Darum ging es aber nicht. Es ging um die Aussage vom Johannes, dass alle
volatiles falsch sind und man niemals volatile verwenden sollte, weil
atomics das können. Das ist falsch.
Johannes schrieb:> volatile sorgt bei jedem einzelnen Zugriff dafür, dass der Wert neu> gelesen wird. Geht mit atomic einwandfrei nachzubilden,
Das ist falsch.
Doch das ist tatsächlich so. Probier es aus.
https://en.cppreference.com/w/cpp/language/cv> Every access (read or write operation, member function call, etc.) made> through a glvalue expression of volatile-qualified type is treated as a> visible side-effect [...]
Im allgemeinen wäre es schön, wenn du auch Beispiele liefern würdest.
Ich mach mir schließlich auch die Mühe. Ohne wenigstens irgend einen
Beleg ist deine Aussage leider nichts wert.
Noch eine Sache dazu, wie Registerzugriffe ohne volatile aussehen
könnten:
1
#include <iostream>
2
#include <cstdint>
3
4
template<typename T>
5
struct Register
6
{
7
Register(uint64_t addr) : _addr(addr) {}
8
9
Register<T>& operator=(const T& value)
10
{
11
// TODO: barrier
12
*(T*)_addr=value;
13
return *this;
14
}
15
16
operator T() const
17
{
18
// TODO: barrier
19
return *(T*)_addr;
20
}
21
22
private:
23
uint64_t _addr;
24
};
25
26
template<typename T, uint64_t TAddr>
27
struct CRegister
28
{
29
CRegister<T, TAddr>& operator=(const T& value)
30
{
31
// TODO: barrier
32
*(T*)TAddr=value;
33
return *this;
34
}
35
36
operator T() const
37
{
38
// TODO: barrier
39
return *(T*)TAddr;
40
}
41
42
operator Register<T>() const
43
{
44
return Register<T>(TAddr);
45
}
46
};
47
48
49
CRegister<int, 0x10000> Reg1;
50
CRegister<int, 0x10004> Reg2;
51
CRegister<int, 0x10008> Reg3;
52
53
void write_argument_register(Register<int> reg)
54
{
55
reg=1234;
56
}
57
58
int main()
59
{
60
Reg1=123;
61
write_argument_register(Reg2);
62
write_argument_register(Reg3);
63
64
return 0;
65
}
Ist ein bare minimum sample. In C++ erzeugt das keinerlei overheat
verglichen mit direktem Zugriff auf die Werte, ich hab mir die
Assemblerausgabe angesehen.
Die Kapselung in Klassen erlaubt einem nun aber, ganz unabhängig vom
Compiler, die Zugriffssicherungen zu implementieren die man möchte. Das
könnte atomic sein, inline asm oder was auch immer.
Johannes schrieb:> Doch das ist tatsächlich so. Probier es aus.
Mir ging es um das "Geht mit atomic einwandfrei nachzubilden" von dir.
Das ist falsch. Ein atomic gibt nicht die gleichen Garantien wie ein
volatile, also kann man das eine nicht mit dem anderen ersetzen.
Atomic garantiert, dass der Zugriff nicht geteilt wird.
Volatile garantiert, dass der Zugriff stattfindet.
Im Übrigen ist dein Beispiel aus meiner Sicht vor allem ein Zeichen für
"viel Boilerplate für erstaunlich wenig Nutzen". Davon hab ich auf
Arbeit schon genug.
Programmierer schrieb:> Axel S. schrieb:>> Es gibt im Kontext dieser>> Wahlmöglichkeiten genau gar keinen Grund, nochmal verschiedene Grade von>> Kaputtheit zu unterscheiden.>> Doch, die Kaputtheit die nur manchmal zuschlägt ist gefährlicher,> weshalb man diese separat betrachten kann.
Du faselst. Schlag das Wort "Kontext" nach.
>> Was fehlt dir denn da?>> Der Hinweis dass im Block auftretende Lese-Zugriffe nicht vor den Block> verschoben werden können, und dass im Block auftretende Schreibzugriffe> nicht nach den Block verschoben werden können ("optimization barrier").
Das heißt, du erwartest, daß die Dokumentation nicht nur sagt: "das
erzeugt einen atomic block", sondern daß sie außerdem noch erläutert,
was ein atomic block ist? Das ist unsinnig. Irgendwas muß man
voraussetzen. Man kann nicht an jeder Stelle anfangen, alles vom
Urschleim her durchzudeklinieren.
>> Natürlich nicht. Das kann so nur der Compiler implementieren.>> Wenn, dann typischerweise aber in Zusammenarbeit mit der libc
_Atomic kann auch als type qualifier verwendet werden. Dann muß der
Code für den Zugriff auf den entsprechenden Typ atomar sein. Das kann
sinnvoll nur der Compiler so erzeugen.
> vermute dass man es zumindest teilweise in C mit Inline-Asm> hinbekommen könnte.
Das ist eine Binsenweisheit. C und ASM sind gleichmächtig. Der
entscheidende Unterschied ist das Abstraktionsniveau. Selbstverständlich
kann man bei jedem Zugriff auf einen "großen" Datentyp jedesmal einen
ATOMIC_BLOCK drum herum bauen (oder in ASM eine CLI/SEI Klammer). Aber
es ist wesentlich sauberer, einfach den Typ als _Atomic zu markieren und
es den Compiler automatisch erzeugen zu lassen.
> Es wird wahrscheinlich ohnehin darauf hinauslaufen, dass die> _Atomic-Funktionen einfach ATOMIC_BLOCK benutzen
_Atomic ist mehr, als das was ATOMIC_BLOCK bietet.
S. R. schrieb:> Darum ging es aber nicht. Es ging um die Aussage vom Johannes, dass alle> volatiles falsch sind und man niemals volatile verwenden sollte, weil> atomics das können. Das ist falsch.
Das konnte ich deinem Beitrag nicht entnehmen.
S. R. schrieb:> Mir ging es um das "Geht mit atomic einwandfrei nachzubilden" von dir.> Das ist falsch. Ein atomic gibt nicht die gleichen Garantien wie ein> volatile, also kann man das eine nicht mit dem anderen ersetzen.
Du hast nicht genau gelesen, was er geschrieben hat. Er behauptet an der
Stelle nicht, dass man mit atomics alle volatiles ersetzen kann. Er
spricht von einer Eigenschaft, die ersetzt werden kann.
S. R. schrieb:> Atomic garantiert, dass der Zugriff nicht geteilt wird.> Volatile garantiert, dass der Zugriff stattfindet.
Das ist beides nicht vollständig und der zweite Teil ist nichtmal
korrekt.
Axel S. schrieb:> Das heißt, du erwartest, daß die Dokumentation nicht nur sagt: "das> erzeugt einen atomic block", sondern daß sie außerdem noch erläutert,> was ein atomic block ist?
Wenn eindeutig ist, was ein atomic block ist, muss es nicht in der Doku
stehen. Wie genau lautet denn die 100% immer korrekte Definiton von
"atomic block"?
Stefan ⛄ F. schrieb:> Ich habe das Gefühl, dass hier jetzt das atomic-block nur noch wegen> seinem Seiteneffekt genutzt wird.
Richtig. Aber da der ATOMIC_BLOCK eben ein ungenauer Holzhammer ist kann
er nicht für Datentypen, die sowieso atomisch sind (uint8_t)
differenzieren.
S. R. schrieb:> Das ist falsch.
Nein.
S. R. schrieb:> Das ist richtig.
Wobei es auch hier Alternativen gibt, die aber nicht unbedingt besser
sind.
S. R. schrieb:> Atomic garantiert, dass der Zugriff nicht geteilt wird.> Volatile garantiert, dass der Zugriff stattfindet.
atomic auch. Du solltest mal was über Memory Ordering lesen:
https://en.cppreference.com/w/cpp/atomic/memory_orderAxel S. schrieb:> sondern daß sie außerdem noch erläutert,> was ein atomic block ist? Das ist unsinnig. Irgendwas muß man> voraussetzen. Man kann nicht an jeder Stelle anfangen, alles vom> Urschleim her durchzudeklinieren.
Da "atomic block" eben keineswegs ein Standard-C oder -C++ -Feature ist,
sondern ein avr-libc-Kuriosum, durchaus. Außerdem fing Cyblord damit an,
über die lückenhafte Doku zu mäkeln.
Axel S. schrieb:> Das kann> sinnvoll nur der Compiler so erzeugen.
Stimmt. In C++ braucht man "nur" eine Klasse std::atomic zu bauen, da
ist das ggf. anders.
Axel S. schrieb:> C und ASM sind gleichmächtig.
Nö. ASM kann plattformspezifische Dinge tun, die sinnvollerweise nicht
Teil von C sind, da sie sich nicht portabel abbilden lassen.
Axel S. schrieb:> _Atomic ist mehr, als das was ATOMIC_BLOCK bietet.
Ja, es ist präziser. Ich meinte, dass die konkrete Implementation der
_Atomic / std::atomic Operationen vermutlich in C oder C++, ggf. mit
etwas Inline-Asm, abbildbar ist. Entweder muss man die dann manuell
aufrufen (ohne _Atomic) oder der Compiler hier nur als Weiche fungieren.
Aber das ist Haarspalterei, ist für die Diskussion kaum relevant.
Hätte man eine std::atomic Implementation für AVR, könnte man o.g.
Programm so schreiben:
Für _Atomic geht es ähnlich, keine Lust es nachzuschlagen.
std::atomic<std::uint8_t> sollte automatisch wissen dass std::uint8_t
atomisch geschrieben werden kann, und dementsprechend keine
Interruptsperre einbauen. Eine gute Implementation vorausgesetzt sollte
hier für AVR der selbe Code herauskommen wie bei volatile.
Da aber der AVR-GCC bzw. avr-libc anscheinend keine Implementation von
std::atomic / _Atomic bietet, muss man sich mit der Krücke ATOMIC_BLOCK
behelfen, welche zwar auch funktioniert aber eben immer den Worst Case
annimmt und die Interrupts sperrt, weil sie nicht zwischen
unterschiedlichen Datentypen differenzieren kann.
Besser verdeutlichen lässt sich das Ganze mit ARMv7, welches Support für
Atomics hat der auch vom GCC unterstützt wird:
Wie zu sehen ist, wird erst die Zahl in einer Schleife atomisch
dekrementiert. Wenn ein anderer Thread (oder ISR) dazwischenfunkt und
die Variable verändert, schlägt das "strex" fehl und es wird erneut
versucht (Zeile "a"). Interessant ist Zeile "12": Hier wird der Wert
erneut geladen, denn der Compiler weiß, dass sich Atomics extern
verändern können, und erzeugt eine neue "ldr" Instruktion.
Zum Vergleich, so sieht es aus ohne atomic:
1
#include<cstdint>
2
3
std::uint32_ttest(std::uint32_t&i){
4
while(i>0){
5
__asm__volatile("");// Schleife nicht wegoptimieren
6
--i;
7
}
8
9
returni;
10
}
Wird zu
1
00000000 <test(unsigned int&)>:
2
0: 6803 ldr r3, [r0, #0]
3
2: b113 cbz r3, a <test(unsigned int&)+0xa>
4
4: 3b01 subs r3, #1
5
6: d1fd bne.n 4 <test(unsigned int&)+0x4>
6
8: 6003 str r3, [r0, #0]
7
a: 2000 movs r0, #0
8
c: 4770 bx lr
9
e: bf00 nop
Kein erneutes Laden. std::atomic hat hier also das "volatile" ersetzt,
und das ohne Interruptsperre (erzeugt der GCC sowieso nicht
automatisch).
Programmierer schrieb:> Rolf M. schrieb:>> Der Optimizer könnte auf die Idee kommen, diese wiederholten Zugriffe>> auf den RAM zu entfernen und den Wert nur in einem Register zu halten.>> Macht er aber nicht, wenn der Zugriff einzeln im ATOMIC_BLOCK steht und> nicht die ganze Schleife. Das haben wir doch jetzt schon 25x diskutiert.
Es ging darum, dass die DOKUMENTATION das nicht erwähnt. Mein Text war
die Antwort auf die Frage, welche Information in der Doku fehlt. Du hast
leider die beiden zusammengehörenden Sätze auseinandergerissen und dann
separat beantwortet.
> Das fehlt lediglich in der Doku.
Genau. Und das war das einzige, was ich damit sagen wollte.
> Rolf M. schrieb:>> Ich dachte, es geht um die Umsetzung des Makros ATOMIC_BLOCK so, dass es>> volatile überflüssig macht,>> Nein, ein solches Makro macht keinen Sinn. Ich rede von einer> Implementation von std::atomic bzw _Atomic.
Ok, dann haben wir tatsächlich von verschiedenen Dingen geredet.