Erstmal noch schöne Weihnachten in die Runde!
Ich nutze gerade die freie Zeit zum Programmieren auf einem STM32,
Programmiersprache c. Prinzipiell ist mir das volatile-Konzept soweit
klar, aber im Detail kommen dann doch Fragen auf.
Die Situation ist folgende: Ein Struct (typedef struct tFoo tFoo;) wird
per Pointer (tFoo * foo) im Kontext von main angelegt und im Kontext des
UART-Interrupts, Timerinterrupts und ggf in main gelesen und Werte darin
verändert. Die erste Frage ist, ob der Struct-Pointer selber schon
volatile sein muss (tFoo * volatile foo), obwohl die Adresse nur einmal
angelegt wird und im folgenden eben nur noch aus verschiedenen Kontexten
zugegriffen wird (1). Die Adresse ändert sich nach dem Anlegen nicht
mehr, man könnte aber argumentieren, dass für den Compiler nicht
ersichtlich ist, ab wann der Wert initialisiert ist und bei besonders
aggressiver Optimierung könnte er auf die Idee kommen den Initialwert
Null zu nehmen, anstatt die (gleiche) Adresse aus dem Pointer bei jedem
Funktionsaufruf neu zu laden.
Als nächstes geht es darum wie mit den Structmembern umzugehen ist. Was
ich so gefunden habe, kommt es auf das gleiche heraus, das ganze Struct
volatile zu definieren (volatile tFoo foo) oder die einzelnen
Membervariablen. Nicht klar ist mir, wie das dann mit Pointern als
Structmember ist. Überträgt sich die Volatile-Definition dann auf die
Adresse oder die Speicherstelle, auf die sie zeigen, oder beides (2)? In
meinem Fall kann sich beides zwischen Kontexten ändern.
Braucht es in meinem Fall "das volle Programm" (3)?
Also:
1
typedefstructtFoo{
2
volatileuint8_t*volatileSomeArray;
3
//...
4
}tFoo;
5
6
volatiletFoo*volatilefoo;
Mit der Vorgehensweise ergeben sich dann diverse Warnungen bei
Funktionsaufrufen, denen eine Adresse übergeben wird: Passing argument
discards volatile qualifier...
Aufruf:
1
somefct(foo+3);//(Der Pointer spannt ein Array auf)
Definition:
1
voidsomefct(tFoo*aFoo);
Meine Recherche dazu hat ergeben, dass man die in meinem Fall wohl
ignorieren kann, weil sich die übergebenen Adressen innerhalb der
Funktionen nicht von außen ändern können, es also reicht, wenn der
Compiler in der Funktion einmal aus der bepointerten Speicherzelle liest
oder rein schreibt. Mit einem Typecast somefct((tFoo*)(tFoo+3)) lässt
sich die Warnung wohl umgehen, die Frage ist aber, ob bei einem
aggressiven Compiler da irgendwas passiern kann (4)? Das ist ja ein
Thema, dass man nicht einfach austesten kann, weil dass es heute
funktioniert heißt ja nicht, dass es sprachlich richtig ist und
entsprechend auch morgen noch funktioniert.
Ich würde mich über Kommentare freuen!
Vielen dank schonmal!
Ein Anfänger schrieb:> Wie sieht das aus?
du willst zwei Probleme lösen:
1) dein main-Programm soll immer die neuesten Werte lesen, die von der
ISR geschrieben wurden
2) das main-Programm soll währenddessen einen gültigen Zustand der
Struktur vorfinden, vor allem soll die ISR nicht während des Zugriffs
die Struktur verändern.
"volatile" löst das erste Problem, in einer "Kanonen auf Spatzen"-Art.
Aber nicht das zweite.
eine compiler barrier löst das erste Problem auch, du teilst dem
Compiler damit mit, dass exakt ab dieser Stelle zwischengespeicherte
Werte verworfen und bei Bedarf wieder frisch aus dem RAM geladen werden
müssen.
Für das zweite Problem brauchst du eine Interrupt-Sperre, solang es
nicht nur ein einzelner bool/int ist.
Jetzt der Clou: Die Interrupt-Sperre bringt (oft/meistens) die
Compiler-Barrier gleich mit, d.H. die Lösung für Problem 2 löst gleich
im Vorbeigehen Problem 1 mit.
z.B. __disable_irq()/STM32/gcc wird zu
1
__ASMvolatile("cpsid i":::"memory");
Ein Anfänger schrieb:> Prinzipiell ist mir das volatile-Konzept soweit> klar
ist es vermutlich nicht.
Das Konzept dahinter ist:
Es ist einfacher, einem Anfänger ein "dann schreib halt volatile davor"
hinzuwerfen, anstatt ihm einen 10-Seiten-Aufsatz über die Freiheiten,
die der C-Standard dem Compiler/Optimizer gewährt, und wie man damit
umgeht, zu schreiben. Den versteht er dann eh nicht.
Εrnst B. schrieb:> du willst zwei Probleme lösen:> 1) dein main-Programm soll immer die neuesten Werte lesen, die von der> ISR geschrieben wurden> 2) das main-Programm soll währenddessen einen gültigen Zustand der> Struktur vorfinden, vor allem soll die ISR nicht während des Zugriffs> die Struktur verändern.
Punkt zwei ist hier nicht relevant, weil alles strikt nacheinander
geschieht, der Zugriff aus einem anderen Kontext findet erst statt, wenn
die Aufgabe im einen Kontext komplett erledigt ist. Es geht also nur
darum, dass innerhalb der Interrupts/Funktionen zumindest einmal
geladen/gespeichert wird.
Das heißt, jeweils eine Memory Barrier am Anfang der ISR müsste schon
ausreichen? Das hieße aber, dass in Funktionen, die in der ISR
aufgerufen werden, die Variablen sowieso nachgeladen werden? Weil der
Compiler "weiß" ja an den Stellen kaum, dass in der ISR schon eine
Memory Barrier war. Ist das der Fall, bräuchte es aber im Grunde gar
keine Memory Barrier, wenn ja sowieso immer nachgeladen wird. Oder
andersrum, benötige ich in jeder Funktion eine Memory Barrier, kann ja
auch nicht sein?
> Es ist einfacher, einem Anfänger ein "dann schreib halt volatile davor"> hinzuwerfen, anstatt ihm einen 10-Seiten-Aufsatz über die Freiheiten,> die der C-Standard dem Compiler/Optimizer gewährt, und wie man damit> umgeht, zu schreiben. Den versteht er dann eh nicht.
Volatile ist aber wie man hier sieht auch nicht gerade selbsterklärend.
Das hier mit Pointern auf Pointer kann auch ein Spezialfall sein, aber
etwas mehr Kommentare zur Volatile-Verwendung hätte ich hier von der
Forengemeinde schon erwartet.
Ein Anfänger schrieb:> Das heißt, jeweils eine Memory Barrier am Anfang der ISR müsste schon> ausreichen?
Nein, vor der Stelle im Main, in der du auf die Datenstruktur zugreifst.
Ein Anfänger schrieb:> Punkt zwei ist hier nicht relevant
d.H. du hast schon sichergestellt, dass die IRQs abgestellt sind,
während auf der Datenstruktur gearbeitet wird, im Hauptprogramm
abgestellt, und keine IRQ-Prioritäten, die nested IRQ-Aufrufe erlauben
würden?
Dann stehen die Chancen gut, dass du garnix weiter brauchst. Kein
Volatile, keine expliziten Optimization-Barriers. Weil die
IRQ-Sperren-Operation das schon implizit mitbringt(*).
*) Hängt natürlich vom Compiler und den verwendeten Frameworks ab. Wenn
du das "von Hand" durch Schreiben der entsprechenden Register machst
(also kein "noInterrupts() / __disable_irq() / HAL_NVIC_DisableIRQ()
usw), dann musst du die Optimization Barrier selber nachrüsten.
Εrnst B. schrieb:> Ein Anfänger schrieb:>> Das heißt, jeweils eine Memory Barrier am Anfang der ISR müsste schon>> ausreichen?>> Nein, vor der Stelle im Main, in der du auf die Datenstruktur zugreifst.
In der ISR greife ich doch auch darauf zu, wo ist der Unterschied zur
main?
> Ein Anfänger schrieb:>> Punkt zwei ist hier nicht relevant>> d.H. du hast schon sichergestellt, dass die IRQs abgestellt sind,> während auf der Datenstruktur gearbeitet wird, im Hauptprogramm> abgestellt, und keine IRQ-Prioritäten, die nested IRQ-Aufrufe erlauben> würden?
Nicht abgestellt, aber durch Flags gesichert. Aus main raus wird
initialisiert, im UART-Interrupt wird ein Flag zur Aufzeichnung gesetzt
und ab dem nächsten Timerinterrupt läuft dann die ADC-Aufzeichnung
jeweils aus dem Timerinterrupt heraus. Wenn die vorgegebene Anzahl
ADC-Messungen in der Struktur abgespeichert ist, wird ein entsprechendes
Flag gesetzt, das in der main-Loop ausgewertet wird und dann für die
Datenübertragung an den PC zuständig ist. Ein Flag sichert ab, dass
während der Übertragung keine neue Messung angestoßen wird. Ich muss
also eigentlich nie Interrupts sperren.
Ich würde auch gerne auf compiler- oder gar target-spezifische Befehle
verzichten, ich dachte, dass man da mit volatile noch am universellsten
unterwegs ist. Aber zumindest Single-Core kann man als gegeben annehmen,
man muss sich also nicht auch noch um den Cache oder so sorgen.
Ein Anfänger schrieb:> Ich würde auch gerne auf compiler- oder gar target-spezifische Befehle> verzichten, ich dachte, dass man da mit volatile noch am universellsten> unterwegs ist.
Dann würde ich ein Blick Richtung C++ empfehlen:
https://en.cppreference.com/w/cpp/header/atomic
Ein Anfänger schrieb:> Ein Flag sichert ab, dass> während der Übertragung keine neue Messung angestoßen wird.
Als "Quick&Dirty" Lösung wäre dann (nur) das flag volatile. Irgendwelche
Array-Elemente wird der Compiler eher nicht in Registern zwischenhalten.
Die "Saubere" Lösung wäre eine optimization barrier in der main vor dem
Test des Flags.
ein
1
asmvolatile("":::"memory");
oder das GCC-Builtin
1
__sync_synchronize();(*)
würde reichen, und wäre nicht Target- aber Compiler-Abhängig.
Ein Anfänger schrieb:> In der ISR greife ich doch auch darauf zu, wo ist der Unterschied zur> main?
Die ISR kann der Compiler nicht irgendwo hin "inlinen", er sieht ja auch
keinen Aufruf davon. Insofern besteht auch keine Gefahr, dass
irgendwelche Code-Teile aus der ISR heraus oder hineingeschoben werden.
*) __sync_synchronize macht neben der Compiler/Optimization-Barrier auch
noch eine Memory-Barrier. Auf Single-CPU-Systemen egal, es wird kein
Code generiert.
Volatile wird schon in den h-Files richtig hinzugefügt, wenn man
IO-Register zugreift.
Z.B. bei der UART sorgt es dafür, daß auch alle Bytes gesendet, gelesen
werden. Der Compiler würde sonst ohne volatile nur das letzte Byte
senden.
Weiterhin braucht man es in der Mainloop, wenn Interrupts eine Variable
auch benutzen, d.h. um mit Interrupts Daten auszutauschen.
Der Interrupt selber braucht es nicht, daher reicht es, auch nur die
Mainzugriffe zu casten:
Εrnst B. schrieb:> Die ISR kann der Compiler nicht irgendwo hin "inlinen", er sieht ja auch> keinen Aufruf davon. Insofern besteht auch keine Gefahr, dass> irgendwelche Code-Teile aus der ISR heraus oder hineingeschoben werden.
OK, verstanden!
Peter D. schrieb:> Weiterhin braucht man es in der Mainloop, wenn Interrupts eine Variable> auch benutzen, d.h. um mit Interrupts Daten auszutauschen.> Der Interrupt selber braucht es nicht, daher reicht es, auch nur die> Mainzugriffe zu casten:>
1
>#defineIVAR(x)(*(volatiletypeof(x)*)&(x))
2
>
Das ist interessant, damit kann man quasi sagen, "genau hier neu laden"?
Dann hätte ich noch eine Frage zu folgender Sequenz,
1
//Global
2
typedefstructtFootFoo;
3
tFoo*foo;//Pointer auf ein Array des Structs
4
5
//In einer Funktion
6
tFoo*localFoo;
7
8
localFoo=(volatiletFoo*)foo+3;//Lade 3tes Array element [*1]
9
localFoo->SomeMember=123456;//[*2]
Ich verstehe es jetzt so, dass durch den Volatile-Cast in *1 die Adresse
vom Array foo definitiv neu geladen wird. Jetzt kann der Compiler auch
nicht mehr die Adresse der Membervariablen aus einem Register holen
lassen oder Ähnliches, d.h. das eine Volatile pflanzt sich in gewisser
Weise über das ganze Struct fort?
Wie ist es dann mit dem Wert in Zeile *2, der Compiler kann durch die
volatile Adresse ja auch nicht mehr wissen, ob der Wert schonmal an
diese Stelle geschrieben wurde, darf damit also das Schreiben auch nicht
wegoptimieren. Ist das so?
Die Zeile *1 sollte dann das gleiche sein wie
Volatile in einer Struktur (offset) ist verboten oder unnütz, bzw nicht
vom Standard definiert, da dieser nur Variablen definiert, nicht
offsets. Deshalb auch die Meldung dass es ignoriert (discard) wird.
In einer Schleife im Main auf ein interruptflag warten ist ein Usecase
für volatile. Zeit abgelaufen, Sendepuffer leer, sowas.
Zwei ganz andere Themen sind Race condition und Daten-Konsistenz. Da
hilft volatile nicht bzw. nur sehr eingeschränkt mit vielen Fallen.
Das Warten in Main ist oft ein Design-flaw. Und sobald in der Schleife
auch ein unbekannter Funktionsaufruf ist, ist volatile "um den herum"
obsolete.
Chris schrieb:> Volatile in einer Struktur (offset) ist verboten oder unnütz, bzw nicht> vom Standard definiert, da dieser nur Variablen definiert, nicht> offsets. Deshalb auch die Meldung dass es ignoriert (discard) wird.
Die Meldung kommt ja, wenn man eine volatile Variable an eine Funktion
übergibt, die nicht volatile ist. So habe ich das zumindest beobachtet.
Bist du sicher, dass das vom Standard nicht definiert ist? Sinnvoll wäre
ja die volatile Verwendung eines einzelnen Structmembers schon.
A. S. schrieb:> Das Warten in Main ist oft ein Design-flaw.
Warten ist vielleicht das falsche Wort, eher ein regelmäßiges pollenn in
der Main-Loop.
> Und sobald in der Schleife> auch ein unbekannter Funktionsaufruf ist, ist volatile "um den herum"> obsolete.
Wie meinst du das?
Komplexes Thema, mit volatile zu arbeiten. Technisch ist volatile wohl
als Qualifikator sowas wie const - d.h. alle Sauereien, die für const
funktionieren, werden vermutlich auch für volatile gehen. Was das
semantisch bewirkt - da bin ich diesbezüglich auch der Meinung, dass das
sehr drauf ankommt.
In C++ kannste mit der const-Correctness eben auch volatile-Correctness
machen. D.h. auf Member einer volatile struct Foo, können dann auch auch
nur volatile-qualifizierte Funktionen zugreifen.
In C ist das meines Wissens nach nicht so - ich glaub, da macht volatile
tatsächlich auch nur für gute alte Daten richtig Sinn. Wann ein
volatiler Pointer (also volatiler Pointer auf nicht-volatile struct)
überhaupt sinnvoll sein kann, ist auch eher schwierig zu beurteilen,
eventuell könnte das für ein Array ne Rolle spielen. Aber selbst das
würde ich beim besten Willen nur sehr defensiv als was Notwendiges
erachten.
Ich glaub, am ehesten macht volatile für structs Sinn, wenn du damit
anzeigen willst, dass die Werte einer struct sich beim nächsten Zugriff
ändern können, ohne dass dein Programm verändernderweise was damit zu
tun hat.
Würde also glauben wollen, dass 'volatile struct* ptr' dafür ausreichen
sollte.
Ja, undefined behavior Ist im Standard als solchen beschrieben.
Manche Embedded C erlauben dies explizit auf diversen Branches des
Compilers, da is es im Manual aber ganz genau beschrieben, dass z.B. das
ganze struct/union/typedef auf volatile promoviert wird, oder dass dies
nur fuer integer und nicht fuer bitfields oder sonstige Variablen
zulaessig ist.
Grundsaetzlich zu GCC:
Gcc does not implement a correct semantics for accesses to volatile
struct members in non volatile objects. Dies ist bekannt und da gcc
sowie g++ eine gemeinsame Codebase haben, wird sich daran auch nie was
aendern, es geht leider nicht.
Dass man header trotzdem sieht mit diesem struct welche ein volatile
struct element enthalten hat einen anderen Grund. In C++(c11 aber auch
frueher)
ist dies ein workaround um keinen copy assignment operator zu haben, und
trotzdem einen constant volatile verwenden zu koennen, da c++(c11) keine
optimierungen, sprich keine constant substitution with fixed constant
value
in Strukturen macht. Damit kann man dann auch readonly register
spezifizieren, welche ansonsten im neueren c++ nicth moeglich gewesen
waere.
db8fs schrieb:> Komplexes Thema, mit volatile zu arbeiten. Technisch ist volatile wohl> als Qualifikator sowas wie const - d.h. alle Sauereien, die für const> funktionieren, werden vermutlich auch für volatile gehen. Was das> semantisch bewirkt - da bin ich diesbezüglich auch der Meinung, dass das> sehr drauf ankommt.>> In C++ kannste mit der const-Correctness eben auch volatile-Correctness> machen. D.h. auf Member einer volatile struct Foo, können dann auch auch> nur volatile-qualifizierte Funktionen zugreifen.>> In C ist das meines Wissens nach nicht so - ich glaub, da macht volatile> tatsächlich auch nur für gute alte Daten richtig Sinn. Wann ein> volatiler Pointer (also volatiler Pointer auf nicht-volatile struct)> überhaupt sinnvoll sein kann, ist auch eher schwierig zu beurteilen,> eventuell könnte das für ein Array ne Rolle spielen. Aber selbst das> würde ich beim besten Willen nur sehr defensiv als was Notwendiges> erachten.
Eigentlich ist es ja so, dass sobald sich die Adresse ändert, auch die
Werte im Struct andere sein können. D.h. die Volatilität müsste sich
übertragen, sonst rechnet der optimierte Code mit (Register-) Werten aus
alten Adressen herum. Anderseits müsste streng genommen bei jedem
Zugriff auf eine Structmember-Variable der Pointer neu dereferenziert
werden, schließlich ist er volatile gekennzeichnet. Das wäre aber für
die Performance schlecht und ich kann mir kein sinnvolles Szenario dafür
vorstellen.
Eigentlich bin ich der Meinung, dass für den Compiler Adressoperationen
bei Pointern sowieso schon so Art Memory-Barriers sein müssten, weil zur
Compile-Zeit die Speicheradresse überhaupt nicht bekannt ist. Der
Compiler kann also nicht auf Registerdaten zurückgreifen (oder ganze
Blöcke wegoptimieren), sondern muss nach jeder Adressoperation neu
laden/speichern.
In dem Fall würde mir dann ein einfacher Volatile-Cast (wie peda
vorgeschlagen hat) reichen, allerdings für den Adresszugriff.
Vermutlich hat der Compiler durch die Funktionsverschachtelungen und die
dynamische Datenstruktur sowieso nie eine Chance dahingehend
Optimierungen durchzuführen, dann müsste es also auch ohne irgend ein
volatile zuverlässig funktionieren. Deswegen ja auch die Frage hier.
Chris schrieb:> Grundsaetzlich zu GCC:> Gcc does not implement a correct semantics for accesses to volatile> struct members in non volatile objects.> [...] es geht leider nicht.
Verstanden, also entweder das komplette Struct ist volatile, oder gar
nichts.
Puh, das Thema ist doch komplizierter wie es auf den ersten Blick
scheint:)
hier darf der Compiler annehmen, dass flg sich nicht mehr ändert und
eine Endlosschleife einbauen.
ändert man die while-Anweisung zu
1
while(flg){myWait();}
mit myWait als externe Funktion, so weis der Compiler nicht, ob flg in
myWait verändert wird. Darum wertet er flg jedes mal aus.
Problematisch wird das nun, wenn er (warum auch immer) doch weiß, dass
flg in myWait (bzw. während des Aufrufs) nicht verändert wird ;-)
Ein Anfänger schrieb:> Puh, das Thema ist doch komplizierter wie es auf den ersten Blick> scheint:)
Deswegen: Compiler/Optimization Barriers. Damit sagst du dem Compiler:
Ab hier möchte ich garantiert frische Werte lesen, falls eine ISR was
geändert hat. Fertig.
Bei einfachen Flags ist deklarieren als "volatile" schon eine brauchbare
Lösung, aber bei komplexeren Datenstrukturen (Ringbuffer für UART z.B.
oder sogar 16/32-Bit Variablen auf 8-Bit µC) brauchst du eh ein
zusätzliches Locking, ATOMIC_BLOCK, IRQ-Sperre etc.
Und genau das bringt die Barrier i.A. schon mit: Problem gelöst.
A. S. schrieb:> mit myWait als externe Funktion, so weis der Compiler nicht, ob flg in> myWait verändert wird. Darum wertet er flg jedes mal aus.>> Problematisch wird das nun, wenn er (warum auch immer) doch weiß, dass> flg in myWait (bzw. während des Aufrufs) nicht verändert wird ;-)
Bei mir ist alles in Funktionen gekapselt, aber da möchte ich mich nicht
ganz darauf verlassen, oder sind Funktionsaufrufe als
Optimierungsgrenzen im Standard definiert? Es werden ja immer die
Optimierungskünste der Compiler gelobt, da würde ich erwarten, dass eine
Funktion, die nur an einer Stelle aufgerufen wird, erstmal geinlined
wird und dann alle Optimierungen darauf angewendet werden, zumindest
potentiell.
Εrnst B. schrieb:> Deswegen: Compiler/Optimization Barriers. Damit sagst du dem Compiler:> Ab hier möchte ich garantiert frische Werte lesen, falls eine ISR was> geändert hat. Fertig.
Wie gesagt, würde ich gerne compilerspezifische Befehle vermeiden.
Ich denke ich nehme jetzt den Volatile-Cast und wenns mir in Zukunft
irgendwann um die Ohren fliegt, habe ich offensichtlich was falsch
gemacht :)
--
Danke an Alle!
Ein Anfänger schrieb:> Die Situation ist folgende: Ein Struct (typedef struct tFoo tFoo;) wird> per Pointer (tFoo * foo) im Kontext von main angelegt
Das hat erstmal nix mit volatile zu tun. Du hast also irgend einen
Zeiger in main, der ab Start des Programms erstmal ins nirwana oder
sonstwo hin zeigt. Der soll aber auf deinen Datensatz zeigen, weswegen
du auf dem Heap den nötigen Platz resevierst und dann deinen Datensatz
initialisierst. Und dessen Adresse landet in obigem Zeiger. Nebenbei:
mit typedef kann man Typen umbenennen (genauer: einen Alias erzeugen),
Prinzip:
typedef altername neuername;
Und ich halte es für eine Unart, dabei den gleichen Wortlaut zu nehmen
und lediglich an der Großschreibung etwas zu schrauben. Aber das nur am
Rande.
Zu volatile: anders als bei Pascal gibt es bei C kein Modulsystem und
damit keine Kapselung. Der Compiler kann bei C also nicht selbst
herausfinden, ob von woanders auf eine Variable zugegriffen werden kann
oder nicht. Also kann er von selbst auch nicht entscheiden, ob die
Variable während des Programmablaufes einer Funktion in einer Quelldatei
(die er gerade übersetzt) von außen geändert werden kann oder nicht. Man
muß es ihm also sagen und dazu dient das volatile.
Um auf deinen Zeiger zurückzukommen: Der soll ja auf deinen Datensatz
auf dem Heap zeigen und nicht woanders hin. Also sollte er auch
gleichbleiben, solange du nicht den Platz für den Datensatz an den Heap
zurückgibst und einen anderen Platz auf dem Heap belegst, um dort erneut
deinen Datensatz anzulegen.
Mein Rat wäre: Steigere dich nicht in immer halsbrecherischere
Konstrukte hinein, sondern formuliere dein Zeug eher bieder und so
einfach wie möglich.
W.S.
> mit myWait als externe Funktion, so weis der Compiler nicht, ob flg in> myWait verändert wird. Darum wertet er flg jedes mal aus.
Das ist undefiniert, weil der Compiler Modulübergreifend optimieren
könnte.
Sinnvollerweise benutzt man (in C++) std::atomic<bool> als Variablentyp.
Volatile mag zwar ein Workarround sein, ist aber streng genommen
undefined behaviour. Was der Pedant zu atomics in C ist, weiß vielleicht
jemand anderes.
Ein Anfänger schrieb:> Eigentlich ist es ja so, dass sobald sich die Adresse ändert, auch die> Werte im Struct andere sein können.
Das hat aber mit volatile nix zu tun. Ein volatile Pointer auf
nicht-volatile Inhalt (int* volatile) - was soll denn das für ein
Pointer sein bzw. was soll der denn deiner Ansicht nach da passieren?
Aus meiner Sicht wär das ein gemeinsam genutzter Pointer (z.B. global),
der durch einen anderen Prozess, Thread oder eine ISR in seinem
Pointer-Wert verändert wird. Wobei ich das sehr fragwürdig finde, nen
Pointer, der eh schon die ärmste Sau im C-Dorf ist, weil er alles und
nix sein kann, dann auch noch mit Nebenläufigkeit zuzumisten ->
eigentlich sollte, finde ich, doch die Bestrebung sein,
Nebenläufigkeiten soweit zu reduzieren wie möglich.
Wie gesagt würde ich als sehr esoterisches Konstrukt einstufen sowas.
D.h. die Volatilität müsste sich
> übertragen, sonst rechnet der optimierte Code mit (Register-) Werten aus> alten Adressen herum. Anderseits müsste streng genommen bei jedem> Zugriff auf eine Structmember-Variable der Pointer neu dereferenziert> werden, schließlich ist er volatile gekennzeichnet. Das wäre aber für> die Performance schlecht und ich kann mir kein sinnvolles Szenario dafür> vorstellen.
Du haust hier Sachen her, die sich eh beißen - entweder nimmste volatile
oder nimmst eben die Performance von normalen Pointern. Ein Pointer auf
ein volatiles Feld macht für genau ein Szenario Sinn: z.B. sowas wie ein
Flag-Register, dass innerhalb der Hardware geändert wird, wo du bei 2
Aufrufen unterschiedliche Daten kriegen kannst. Das gehört nicht rein,
um Threads oder ISRs zu pseudo-'synchronisieren' oder was auch immer -
sondern nur für den einen Fall. Und weil das so ein tolles Szenario ist,
sollte deine Funktion, die volatile-Zeug nutzt, das ja auch nur jeweils
an einer Stelle tun.
> Eigentlich bin ich der Meinung, dass für den Compiler Adressoperationen> bei Pointern sowieso schon so Art Memory-Barriers sein müssten, weil zur> Compile-Zeit die Speicheradresse überhaupt nicht bekannt ist. Der> Compiler kann also nicht auf Registerdaten zurückgreifen (oder ganze> Blöcke wegoptimieren), sondern muss nach jeder Adressoperation neu> laden/speichern.
Mal blöd gefragt: was macht eigentlich ein Linker so schönes? Bzw. wie
entstehen überhaupt die kleinen Adressen? Memory-Barriers - wer soll das
denn machen, wenn nicht du? Das Betriebssystem oder die C-Runtime? Dann
kompilier mal C für'n 6510...
> In dem Fall würde mir dann ein einfacher Volatile-Cast (wie peda> vorgeschlagen hat) reichen, allerdings für den Adresszugriff.
Wie gesagt, Empfehlung: Adress nix volatile, Inhalt flüchtig.
Weglaufende Adressen nix gut. Sonst eben: viel Spaß beim Debuggen, wenn
du nicht mal weißt, wer gerade fröhlich deinen Pointer geändert hat.
> Vermutlich hat der Compiler durch die Funktionsverschachtelungen und die> dynamische Datenstruktur sowieso nie eine Chance dahingehend> Optimierungen durchzuführen, dann müsste es also auch ohne irgend ein> volatile zuverlässig funktionieren. Deswegen ja auch die Frage hier.
Guck dir den kleinsten denkbaren Prozessor an, für den es einen
C-Kompiler gibt und erzeug deinen ASM-Code dafür mit und ohne volatile.
Aber so wie du glaubst dein Programm mit volatile programmieren zu
können, glaub ich, wird der Compiler vermutlich so oder so nix mehr groß
optimieren können - weil da müsste er deine Zugriffe auf die Adresse ja
irgendwie tracken/referenzieren können.
Wenn du optimieren willst, nimm 'restrict'.
W.S. schrieb:> Zu volatile: anders als bei Pascal gibt es bei C kein Modulsystem und> damit keine Kapselung. Der Compiler kann bei C also nicht selbst> herausfinden, ob von woanders auf eine Variable zugegriffen werden kann> oder nicht. Also kann er von selbst auch nicht entscheiden, ob die> Variable während des Programmablaufes einer Funktion in einer Quelldatei> (die er gerade übersetzt) von außen geändert werden kann oder nicht. Man> muß es ihm also sagen und dazu dient das volatile.
Da irrst du (wie so häufig) mal wieder. Der Compiler kann, wie oben
schon erwähnt wurde, modulübergreifend optimieren und erkennen, ob eine
Variable in einer anderen Quelldatei geändert werden kann.
volatile braucht es erst, wenn Änderungen auftreten können, die von
außerhalb der dem Compiler bekannten Welt ausgehen bzw. dorthin
Seiteneffekte haben. Anders ausgedrückt: wenn die Hardware dem Compiler
den Boden unterm Arxxx wegzieht.
Und das wäre bei Pascal ganz genauso, wenn das denn überhaupt irgendwie
mit Hardware interagieren könnte.
Oliver
Oliver S. schrieb:> Da irrst du (wie so häufig) mal wieder. Der Compiler kann, wie oben> schon erwähnt wurde, modulübergreifend optimieren und erkennen, ob eine> Variable in einer anderen Quelldatei geändert werden kann.
Das macht eher der Linker mit Link-Time-Code.
> volatile braucht es erst, wenn Änderungen auftreten können, die von> außerhalb der dem Compiler bekannten Welt ausgehen bzw. dorthin> Seiteneffekte haben. Anders ausgedrückt: wenn die Hardware dem Compiler> den Boden unterm Arxxx wegzieht.
Dafür ist eben das Szenario volatile struct durchaus auch fragwürdig.
Mal angenommen du hast sowas auf 32-bit-System:
Mal weiter angenommen, 'TollesBitfield' wird eben durch die Hardware im
Hintergrund an dem Addressoffset verändert, ohne dass der Linker was
davon mitkriegt. Was er auch nie-niemals mitkriegen kann, weil Inhalte
vom Speicher halt eben zur Programmlaufzeit erst entstehen (und nicht
zur Compile/Linking-Time).
Also was passiert beim Zugriff darauf?
-> Erstmal ist klar: cast auf (Foo*) muss her, weil volatile nur
beschrieben werden darf.
Nun willste aber am Liebsten ne Momentaufnahme der ganzen struct haben.
In C++ könnte man da Foo::operator=() volatile überschreiben und die
Felder einzeln const_casten und dann auslesen.
-> aber was machste in normalem C zum Kopieren einer struct? Eigentlich
meistens memcpy:
Wobei ich mir nicht ganz sicher bin, ob das memcpy hier wirklich in
jedem Fall gut gehen würde (memcpy weiß ja nix mehr von volatile). Und
niemand garantiert, dass während des memcpy nicht dein Speicher im
Hintergrund geändert wird.
Das Problem hätteste aber auch, wenn nur eines der struct-Felder
volatile wäre.
D.h. das Coding mit volatile und die Sachen, die der Compiler damit
macht zwingt einen fast dazu, volatile nicht zu benutzen oder wenn, dann
nur um ganz einfache Daten sich als Wort zu holen, dann zu casten und
normal zu weiter zu verwenden.
> Und das wäre bei Pascal ganz genauso, wenn das denn überhaupt irgendwie> mit Hardware interagieren könnte.
Kann's glaube ich. Ich mein mich erinnern zu können, dass es da auch ne
Art Referenz-Mechanismus gab und sogar auch inline-Asm damit ging.
db8fs schrieb:> Oliver S. schrieb:>> Da irrst du (wie so häufig) mal wieder. Der Compiler kann, wie oben>> schon erwähnt wurde, modulübergreifend optimieren und erkennen, ob eine>> Variable in einer anderen Quelldatei geändert werden kann.>> Das macht eher der Linker mit Link-Time-Code.
Nee, auch wenn’s link time optimization heißt, ist das doch immer noch
ein Compiler, der da tätig ist.
Oliver
Oliver S. schrieb:> Nee, auch wenn’s link time optimization heißt, ist das doch immer noch> ein Compiler, der da tätig ist.
Meinste? Ich hatt's irgendwie so in Erinnerung, dass das Visuellste
aller Studios nur bei vollen Linkerwarnungen unreferenzierte Variablen
erkennt.
Ist aber auch egal, vermutlich haben wir beide Recht, weil der Compiler
ja auch irgendeine Instrumentierung da mit reinmehren muss.
Oliver S. schrieb:> Da irrst du (wie so häufig) mal wieder. Der Compiler kann, wie oben> schon erwähnt wurde, modulübergreifend optimieren und erkennen, ob eine> Variable in einer anderen Quelldatei geändert werden kann.
Ach, du meinst, weil du eine schwammige Ansicht über die Fähigkeiten
eines C-Compilers (hier zumeist NUR der GCC) hast, würden alle anderen
irren?
Wenn ein C-Compiler eine Quelldatei übersetzt, dann kann er prinzipiell
eben nicht wissen, ob es da noch eine andere Quelldatei gibt, die auf
Zeugs in der aktuellen Datei zugreifen kann oder nicht. Er müßte dazu
alle anderen (und noch nicht übersetzten) Dateien durchforsten, ob da
die aktuell zugehörige .h includiert wird oder ob ein 'extern ...'
irgendwo drinsteht, worüber dann der Zugriff erfolgt. Der Linker hat da
zwar die Möglichkeit dazu, aber er linkt nur und übersetzt nicht und er
ist erst dran, wenn die eigentliche Übersetzungsarbeit bereits erledigt
ist. Schließlich ist er nicht der Compiler. Soviel zu der Realität.
Ich sehe hier immer wieder, daß die C-Fans ihrem GCC geradezu magische
Fähigkeiten zuschreiben. Nein Oliver, da irrst du dich (wie so häufig)
mal wieder.
W.S.
Oliver S. schrieb:> Und das wäre bei Pascal ganz genauso, wenn das denn überhaupt irgendwie> mit Hardware interagieren könnte.
Warum sollte Pascal nicht mit Hardware interagieren können? Hier irrst
offensichtlich Du.
Nur weil bei der µC-Programmierung C der Platzhirsch ist, bedeutet dies
nicht das andere Programmiersprachen, einschließlich Pascal, nicht mit
der HW des µC's umgehen können. Ob eine Programmiersprache mit HW
umgehen kann ist keine Frage der Sprache selbst, sondern es hängt am
Ende vom Compiler ab, ob er dies entsprechend umsetzen kann.
Free Pascal, RONPAS (basiert auf FPC) oder auch mikroPascal
(https://www.mikroe.com/mikropascal-avr), um nur mal 3 Beispiele zu
nennen, können das. Letztere scheinen damit sogar Geld zu verdienen. Es
gibt sogar Firmen die mit in Pascal geschriebener Software für µC Geld
verdienen -
https://www.emotas.de/allgemein/embedded-software-entwicklung-mit-pascal.
Du gehörst ganz offensichtlich zur Gilde derer, die meinen C sei der
Nabel der Welt und daneben gäbe es nichts Anderes. Gott sei dank ist dem
nicht so und auch im Bereich der µC-Programmierung ist die Welt bunter
als manche meinen.
W.S. schrieb:> Der Linker hat da> zwar die Möglichkeit dazu, aber er linkt nur
so ist bei modernen Toolchains eben nicht.
Schon der ziemlich alte Keil C51 hatte die Optimierung zur Linkzeit
optional zur verfügung. Dann werden die C Files ggv mehrmals übersetzt.
LTO (LinkTimeOptimization) ist inzwischen ein weitverbreitets Feature.
W.S. schrieb:> Wenn ein C-Compiler eine Quelldatei übersetzt, dann kann er prinzipiell> eben nicht wissen, ob es da noch eine andere Quelldatei gibt, die auf> Zeugs in der aktuellen Datei zugreifen kann oder nicht.
Bei globalen Variablen kann der Compiler – ohne sich alle anderen Module
anzuschauen (also ohne LTO) – nicht wissen, welche dieser Module auf die
Variablen zugreifen. Bei nichtglobalen Variablen weiß der Compiler
sicher, dass keine anderen Module darauf zugreifen. Entsprechendes gilt
für Funktionen.
Das ist in Pascal so, und das ist in C so, da gibt es keinen
Unterschied.
Die "Modulsysteme" von Pascal und C unterscheiden sich im Wesentlichen
nur darin, dass die Interface-Beschreibung bei C in einer vom Menschen
lesbaren .h-Datei und bei Free Pascal in einer binären
.ppu-Datei liegt.
Yalu X. schrieb:> Die "Modulsysteme" von Free Pascal und C unterscheiden sich im> Wesentlichen> nur darin, dass die Interface-Beschreibung bei C in einer vom Menschen> lesbaren .h-Datei und bei Free Pascal in einer binären .ppu-Datei liegt.
Die Modulsysteme von Pascal und C unterscheiden sich schon erheblich.
In Pascal reicht es, wenn man die binäre Unit (bei FPC eben die *.ppu)
in der uses-Liste aufführt, damit der Compiler die exportierten
Funktionen/Variable der Unit ins Compilat einbinden kann. Voraussetzung
ist allerdings das die Binärdatei mit der gleichen Compilerversion
compiliert wurde - das Komponentensystem bei Delphi/Lazarus funktioniert
genauso, d.h. ich brauche keine Quellen. Die Quellen brauche ich nur,
wenn ich die Unit neu übersetzen muß, weil sie z.B mit einer anderen
Compilerversion erzeugt wurde. Dann würde sie beim ersten Aufruf der
Unit im Gesamtprojekt neu kompiliert werden. Ich kann die Unit auch in
mehreren Projektdateien einbinden. Dazu muß ich nich so ein Gehampel wie
bei den *.h Includedateien machen und im Quelltext solch ein Konstrukt
1
#ifndef __MSP430WARE_ADC10_A_H__
2
#define __MSP430WARE_ADC10_A_H__
3
.
4
.
5
#endif
bemühen, damit ich am Ende nicht den Linker durch mehrfaches Includieren
ins Schleudern bringe.
Bei C wird eben genau das gemacht was der Bergriff include aussagt. Das
Bibliothekskonzept bei C ist ein völlig anderes als bei Pascal. Bei C#
greift man ein ähnliches Konzept wie bei Pascal auf. Aber wen wundert
es, wenn der Chefentwickler von C#, Anders Hejlsberg, federführend an
der Entwicklung von Delphi beteiligt war.
Zeno schrieb:> Die Modulsysteme von Pascal und C unterscheiden sich schon erheblich.> ...> Die Quellen brauche ich nur, wenn ich die Unit neu übersetzen muß,> weil sie z.B mit einer anderen Compilerversion erzeugt wurde.
Wenn der kompilierte Code noch aktuell ist, muss der Quellcode auch in C
nicht neu übersetzt werden. Sowohl in Free Pascal und als auch in C
braucht man in diesem Fall nur das Kompilat (*.o) sowie die
Interface-Beschreibung (*.ppu in Free Pascal bzw. *.h in C). Da sehe ich
keinen relevanten Unterschied.
> Dazu muß ich nich so ein Gehampel wie bei den *.h Includedateien> machen und im Quelltext solch ein Konstrukt>> #ifndef __MSP430WARE_ADC10_A_H__> #define __MSP430WARE_ADC10_A_H__> .> .> #endif>> bemühen, damit ich am Ende nicht den Linker durch mehrfaches Includieren> ins Schleudern bringe.
Das ist einer der wenigen Fälle, wo man in C tatsächlich mehr tippen
muss als in Pascal. Dieser Nachteil wird aber in vielen anderen
Bereichen wieder mehr als kompensiert. Ganz abgesehen davon ist das
wesentlich kürzere
1
#pragma once
schon seit Langem De-facto-Standard.
> Bei C wird eben genau das gemacht was der Bergriff include aussagt. Das> Bibliothekskonzept bei C ist ein völlig anderes als bei Pascal.
Das Header-File-Konzept in C ist mächtiger als das PPU-Konzept in
Pascal. Man kann es – wenn man möchte – genau so benutzen wie die PPUs
in Pascal, man kann aber auch ganz andere Dinge damit anstellen, die in
Pascal nicht möglich sind.
Wir schweifen aber langsam immer mehr vom Thread-Thema ab.
Yalu X. schrieb:> Das ist einer der wenigen Fälle, wo man in C tatsächlich mehr tippen
Das geht noch nicht einmal um's mehr tippen, das ist einfach nur grottig
und eine ganz böse Falle für Anfänger, wenn man dieses Konstrukt vergißt
oder es mangels Erfahrung nicht weiß. Das Üble an der Sache ist das der
Compilerlauf fehlerfrei ist und der Linker die Grätsche macht. Mit der
Fehlermeldung "duplicate symbol ..." kann man Anfangs nicht viel
anfangen, man versteht es schlichtweg nicht.
Yalu X. schrieb:> Das Header-File-Konzept in C ist mächtiger als das PPU-Konzept in> Pascal.
Das halte ich für ein Gerücht. Aber das ist ganz gewiß Ansichtssache.
Yalu X. schrieb:> man kann aber auch ganz andere Dinge damit anstellen, die in> Pascal nicht möglich sind.
die da wären?
Wenn dieses Headerfilekonzept so toll ist, warum kommt man dann in C#
davon ab und implementiert ein dem Pascal sehr ähnliches
Bibliothekskonzept? Die Einbindung der Bibliotheken erfogt dort ähnlich
wie bei Pascal, sogar mit dem (fast) gleichen Schlüsselwort. Auch der
interne Aufbau der Bibliotheken ist weit von dem .h/.c-Konzept in C
entfernt.
db8fs schrieb:> Ein Anfänger schrieb:>> Eigentlich ist es ja so, dass sobald sich die Adresse ändert, auch die>> Werte im Struct andere sein können.>> Das hat aber mit volatile nix zu tun. Ein volatile Pointer auf> nicht-volatile Inhalt (int* volatile) - was soll denn das für ein> Pointer sein bzw. was soll der denn deiner Ansicht nach da passieren?
Ich tu mir ja auch schwer mit dem Thema :-)
Ein kurzes Beispiel, ein Funktionspointer fctPointer wird sowohl in den
Interrupts als auch in Main einer Funktion zugewiesen und auch genutzt,
aber durch die Flags garantiert nicht gleichzeitig.
1
intmain(void)
2
{
3
//[...]
4
5
while(1)
6
{
7
if(SomeFlag){
8
fctPointer=&HalloWelt;
9
fctPointer();
10
}
11
}
12
}
Der Compiler könnte jetzt sagen, "aha, der Funktionspointer ändert sich
ja zwischen den Schleifen gar nicht". Und die Zuweisung vor die
While-Schleife setzen und nur noch den Aufruf in der Schleife belassen.
In den Interrupts hat der Pointer aber eine andere Funktion, die
Optimierung würde also die Programmlogik zerstören. Jetzt ist das sicher
nicht ideal, den Pointer wieder zu verwenden, aber das soll nur mal ein
vereinfachtes Beispiel sein.
Ich denke mir einfach, wenn der Compiler eine einfache
Boolean/Int-Variable wegoptimieren kann, warum dann nicht auch ein
ganzes verpointertes Structarray, in dem eine Datenstruktur und
Funtionspointer hinterlegt sind.
> Aus meiner Sicht wär das ein gemeinsam genutzter Pointer (z.B. global),> der durch einen anderen Prozess, Thread oder eine ISR in seinem> Pointer-Wert verändert wird. Wobei ich das sehr fragwürdig finde, nen> Pointer, der eh schon die ärmste Sau im C-Dorf ist, weil er alles und> nix sein kann, dann auch noch mit Nebenläufigkeit zuzumisten ->> eigentlich sollte, finde ich, doch die Bestrebung sein,> Nebenläufigkeiten soweit zu reduzieren wie möglich.
Es geht um eine Loggerfunktionalität, der ein statischer Speicherbereich
zugewiesen wird. Die Loggerfunktionalität muss dann mit dem Speicher
auskommen, und auch darin die Konfiguration der unterschiedlichen
Loggingkanäle. Da die Kanalanzahl und der -typ benutzerspezifisch ist
und sich auch im Programmablauf ändern könnte (typischerweise im
UART-Interrupt), muss also auf dem Speicherbereich mit Pointern operiert
werden. Und dieses Konstrukt gilt es abzusichern.
Wie gesagt, glaube ich mittlerweile, dass sich der Compiler bei Pointern
sowieso sehr zurückhält mit Optimierungen, einfach weil er die Adressen
beim Compilieren noch nicht kennt. Dürfte also von vornherein "safe"
sein.
Yalu X. schrieb:> W.S. schrieb:>> Wenn ein C-Compiler eine Quelldatei übersetzt, dann kann er prinzipiell>> eben nicht wissen, ob es da noch eine andere Quelldatei gibt, die auf>> Zeugs in der aktuellen Datei zugreifen kann oder nicht.>> Bei globalen Variablen kann der Compiler – ohne sich alle anderen Module> anzuschauen (also ohne LTO) – nicht wissen, welche dieser Module auf die> Variablen zugreifen. Bei nichtglobalen Variablen weiß der Compiler> sicher, dass keine anderen Module darauf zugreifen. Entsprechendes gilt> für Funktionen.>> Das ist in Pascal so, und das ist in C so, da gibt es keinen> Unterschied.
Der Grund, dass man in Pascal kein volatile braucht/kennt, ist, weil
dort nur lokale Variablen überhaupt optimiert werden. Das kostet ein
wenig Performance aber vereinfacht Vieles. (Bin nur "ein Anfänger" in c,
nicht in Pascal).
> Die "Modulsysteme" von Pascal und C unterscheiden sich im Wesentlichen> nur darin, dass die Interface-Beschreibung bei C in einer vom Menschen> lesbaren .h-Datei und bei Free Pascal in einer binären> .ppu-Datei liegt.
Die ppu ist letztlich nur ein Compilerinterna, das braucht den
Programmierer gar nicht zu interessieren. Die Interface-Beschreibung in
Pascal-Units liegt in der Quellcode Datei in der (Trommelwirbel..)
Interface-Section. Eine Unit ist halt einfach die Kombi aus Source und
Header. Der eigentliche Unterschied ist, dass die c-header in beliebig
viele Quelldateien eingebunden werden und man dadurch zwischen
Deklaration und Definition unterscheiden muss und das zu "extern" Orgien
führt. Bei Pascal-Units kümmert sich der Kompiler darum. Zweitens führt
der Compiler ein Namespacing durch, man hat also kein Problem mit
gleichlautenden nicht veröffentlichten Variablen.
Außerdem, dass sich der Pascal-Compiler selbst alle erforderlichen Units
zusammen sucht und kompiliert, während man dem c-Compiler erstmal sagen
muss, was er überhaupt vorkompilieren soll. Letzteres ist vielleicht der
Punkt, auf den W.S. heraus will. Ich verstehe ehrlich gesagt nicht,
warum c nicht auch ein Unit-System verwendet, kann ja gerne für volle
Rückwärtskompatibilität parallel zu Headerdateien funktionieren. Aber
der Komfort ist einfach um so viel größer.
W.S. schrieb:> Oliver S. schrieb:>> Da irrst du (wie so häufig) mal wieder. Der Compiler kann, wie oben>> schon erwähnt wurde, modulübergreifend optimieren und erkennen, ob eine>> Variable in einer anderen Quelldatei geändert werden kann.>> Ach, du meinst, weil du eine schwammige Ansicht über die Fähigkeiten> eines C-Compilers (hier zumeist NUR der GCC) hast, würden alle anderen> irren?
Nein. Ein C-Compiler kann es prinzipiell. Das es nicht alle können,
spielt dabei keine Rolle.
Was ich aber eigentlich aussagen wollte: volatile hat mit der Fähigkeit
des Compilers, über die Grenzen von Übersetzungseinheiten hinaus schauen
zu können, überhaupt nichts zu tun. Auch wenn er es nicht kann, braucht
es kein volatile, solange die Hardware es nicht erfordert.
Daher geht dein ganzes Geschreibsel über Module und
Übersetzungseinheiten völlig am Thema vorbei. Du irrst, und das wie
üblich sowas von gründlich, gründlicher geht gar nicht.
Oliver
Ein Anfänger schrieb:> Ich tu mir ja auch schwer mit dem Thema :-)> Ein kurzes Beispiel, ein Funktionspointer fctPointer wird sowohl in den> Interrupts als auch in Main einer Funktion zugewiesen und auch genutzt,> aber durch die Flags garantiert nicht gleichzeitig.> int main(void)> {> //[...]> while (1)> {> if(SomeFlag){> fctPointer= &HalloWelt;> fctPointer();> }> }> }>> Der Compiler könnte jetzt sagen, "aha, der Funktionspointer ändert sich> ja zwischen den Schleifen gar nicht". Und die Zuweisung vor die> While-Schleife setzen und nur noch den Aufruf in der Schleife belassen.> In den Interrupts hat der Pointer aber eine andere Funktion, die> Optimierung würde also die Programmlogik zerstören. Jetzt ist das sicher> nicht ideal, den Pointer wieder zu verwenden, aber das soll nur mal ein> vereinfachtes Beispiel sein.
Dein Beispiel ist, glaube ich, weit hergeholt und zeigt ein wenig die
Herausforderungen bei parallelen Abläufen. Das problematische an deinem
Beispiel ist, dass dein Kontrollfluss so sogar randomisiert werden kann
(wenn du nem Pointer zugestehst, sich quasi auch beliebig außerhalb der
Programmgrenzen ändern zu dürfen...
Was nämlich de facto in deinem Beispiel passiert: Du willst einer ISR
eine Dependency Injection per Callback/Funktionspointer verpassen.
Grundsätzlich kann man das machen, auch wenn ISRs "eigentlich" (tm) so
kurz und so zustandsfrei wie möglich sein sollten. Heißt: idealerweise
möglichst wenig gemeinsamer Code zwischen ISR und Hauptprogramm.
Selbst dafür brauchste keinen Pointer der volatile ist. Dein
Funktionspointer-Type wäre ja in deinem Beispiel glaube sowas wie "void
(* volatile)()" (Argumente fürn Stack jetzt mal weggelassen).
Das ist nicht nötig, denn wenn deine ISR ausgeführt wird, ist ja idR
irgendein Global-Interrupt-Flag aktiv, was dazu führt, dass über deinen
Interruptvektor an die Stelle deiner ISR gebrancht wird - das
Hauptprogramm wird erst am Ende der ISR wieder erreicht.
Heißt: aus Prozessorsicht pseudoserielle Abarbeitung - es wird nicht
passieren, dass dein Funktions-Pointer zeitgleich von mehreren
"Threads", "ISRs" oder was auch immer verändert wird.
> Ich denke mir einfach, wenn der Compiler eine einfache> Boolean/Int-Variable wegoptimieren kann, warum dann nicht auch ein> ganzes verpointertes Structarray, in dem eine Datenstruktur und> Funtionspointer hinterlegt sind.
Der Compiler optimiert in erster Linie weg, sofern es die
Referenzierungen der Variable zulassen - so ist mein Verständnis davon.
Wenn du ein Array von Structs anlegst und irgendwo auch darauf
zugreifst, wird da keiner was wegoptimieren.
> Es geht um eine Loggerfunktionalität, der ein statischer Speicherbereich> zugewiesen wird. Die Loggerfunktionalität muss dann mit dem Speicher> auskommen, und auch darin die Konfiguration der unterschiedlichen> Loggingkanäle. Da die Kanalanzahl und der -typ benutzerspezifisch ist> und sich auch im Programmablauf ändern könnte (typischerweise im> UART-Interrupt), muss also auf dem Speicherbereich mit Pointern operiert> werden. Und dieses Konstrukt gilt es abzusichern.
Hmm, ok. Logging ist ja ne querschnittliche Sache, die theoretisch
überall vom Programm aus genutzt werden kann. Machs nicht zu
kompliziert, lieber einen Kanal und da nen String-Präfix vor jede Zeile
der Textausgabe. Das Logging kannste mit Skripten außerhalb deiner
Appliance vermutlich besser auswerten. Ist zumindest meine Erfahrung mit
Logging.
Ansonsten würde für dein Logging eigentlich sowas als Funktionspointer
reichen:
> Wie gesagt, glaube ich mittlerweile, dass sich der Compiler bei Pointern> sowieso sehr zurückhält mit Optimierungen, einfach weil er die Adressen> beim Compilieren noch nicht kennt. Dürfte also von vornherein "safe"> sein.
Optimierungen sind erst relevant, wenn's Probleme gibt. Vorher lohnt
sichs nicht.
Die `volatile`-Qualifizierung wird nur für sog. besondere
Speicherzellen benötigt.
Sie wird nie benötigt und ist auch falsch für normale Speicherzellen
im Sinne
des C/C++-Speichermodells: also auch nicht für Variablen mit
nebenläufigem Zugriff.
Alle Howtos und Tutorials, die derartiges behaupten, sind schlicht
falsch.
In C/C++ ist es grundsätzlich UB, wenn nebenläufig (mit zwei oder mehr
Aktivitätsträger oder
auch `main()` und `ISR`) auf dieselben Speicherzellen zugegriffen
wird, sofern nicht
* atomare Operationen, oder
* eine strenge happens-before Beziehung
garantiert wird.
`volatile` bedeutet nicht atomar!
'volatile' etabliert auch keine strenge happens-before Relation
zwischen konkurrierenden
Zugriffen auf dieselbe Spiecherzelle. Dies man man nur mit geeigneten
Synchronisationsprimitiven
wie `mutex` erreichen (zwei oder mehr Aktivitätsträger), oder aber mit
mit anderen ausreichenden
Mitteln wie Interrupt-Sperre zusammen mit einer Memory-Barrier (sowohl
Compiler- als auch
CPU-Memory-Barrier). Das letztere ist dann eine implementation-defined
Variante.
Daher: `volatile` ist nicht-geeignet und - allein eingesetzt - falsch
für nebenfäufigen Zugriff auf
dieselben Objekte. In C/C++ heisst das dann ein conflict, der wie oben
gelöst werden muss, um nicht
in UB zu enden.
`volatile` hat die folgenden Eigenschaften:
* kein atomarer Zugriff,
* Verschiebung einer Lese-Operation einer normalen Speicherzelle bzgl.
`volatile` ist möglich,
* Verschiebung von Operationen auf `volatile` untereinander ist nicht
möglich.
Damit eignet sich `volatile` nur für Operationen auf memory-mapped
HW-Registern (und ist auch nur
genau dafür erfunden worden). Denn diese besonderen Speicherzellen
haben folgende Eigenschaften:
* sie haben einen Seiteneffekt, und
* ihre Werte erscheinen nicht stabil: ein Lesen nach einem Schreiben
muss nicht denselben Wert ergeben
wie auch zwei aufeinander folgende Lese-Operationen nicht denselben Wert
ergeben müssen, und
* sie haben eine semantische Abhängigkeit: das Lesen/Schreiben eines
HW-Registers beeinflusst das
Lesen/Schreiben eines anderen HW-Registers.
(Achtung: in anderen Sprachen als C/C++ wie etwa Java hat `volatile`
eine andere Bedeutung.)
Für den nebenläufigen Zugriff auf dieselben Variablen / Datenstrukturen
bleiben also nur
* atomare Datentypen (`_Atomic` bzw. `std::atomic<>`), oder
* explizite Synchronisation durch:
- `pthread_mutex_lock()`/ `pthread_mutex_unlock()` oder `std::mutex`
oder ähnliche, oder
- explizites Abschalten der Nebenläufigkeit zusammen mit einer
Memory-Barrier
Für die Kommunikation zwischen `ISR` und `main()` bzw. weiteren `ISR`
benutzt man daher
eine geeignete Interrupt-Sperre (Abschalten der Nebenläufigkeit) und
eine Memory-Barrier (immer
eine Compiler-Barrier und falls nötig eine CPU-Barrier). `volatile` ist
aus den o.g. Gründen hier
falsch.
Diese ganzen Betrachtungen gelten generell und haben erstmal gar nichts
mit heftigen Optimierungen
eines Compilers zu tun. Aber natürlich werden bestimmte Effekte bei der
Optimierung und damit
der Ausnutzung der Regeln für normale Speicherzellen besonders
sichtbar.
Im übrigen bedeutet `_Atomic` oder `std::atomic<>` nicht, dass Operation
nicht optimiert werden:
Auch manche Operationen bzgl. atomarer Datentypen können zusammengefasst
werden wie etwa
aufeinanderfolgende Schreiboperationen. Nur tun das die meisten Compiler
(derzeit) noch nicht.
Wilhelm M. schrieb:> In C/C++ ist es grundsätzlich UB, wenn nebenläufig (mit zwei oder mehr> Aktivitätsträger oder> auch `main()` und `ISR`) auf dieselben Speicherzellen zugegriffen> wird, sofern nicht>> * atomare Operationen, oder> * eine strenge happens-before Beziehung>> garantiert wird.
Danke für die Ausführung. M.E. wäre sie wert, ein eigener Wiki-Artikel
zu volatile hier zu werden. Mit 2-3 Quellenangaben/Links,
(happens-before, UB wenn nebenläufig ...)
db8fs schrieb:> Dein Beispiel ist, glaube ich, weit hergeholt und zeigt ein wenig die> Herausforderungen bei parallelen Abläufen.
Ich beschreibe es weiter unten nochmal genauer.
> Heißt: aus Prozessorsicht pseudoserielle Abarbeitung - es wird nicht> passieren, dass dein Funktions-Pointer zeitgleich von mehreren> "Threads", "ISRs" oder was auch immer verändert wird.
Wie gesagt, finden die Zugriffe in meinem Fall sowieso flaggeschützt
streng nacheinander statt, meine einzige Sorge ist also das
Wegoptimieren.
> Wenn du ein Array von Structs anlegst und irgendwo auch darauf> zugreifst, wird da keiner was wegoptimieren.
Dann müstte es ja passen. Tatsächlich hat auch alles von Anfang an ohne
volatile funktioniert, aber ich wollte halt sicher gehen.
> Machs nicht zu> kompliziert, lieber einen Kanal und da nen String-Präfix vor jede Zeile> der Textausgabe.
Jetzt beschreib ich doch noch mal genauer um was es geht, ich wollte
hier halt die Diskussion nicht noch mit Details verkomplizieren:
Der Logger ist um Daten zeitlich zu plotten. Der Logger-Unit weist man
den Speicherbereich zu (sinngemäß loggerInit(&Buffer,BufferLen);)
anschließend definiert man Kanäle für jeweils einen bestimmten Datentyp
und übergibt einen Pointer auf die Variable, die zyklisch abgespeichert
werden soll, sinngemäß loggerAddUInt16Channel(&ADCValue1);
loggerAddDigitalChannel(&buttonPressed);
Man kann also mehrere Kanäle mit verschiedenen Datentypen definieren,
der Logger berechnet dann wieviele der Werte (aller Kanäle) in den
Buffer passen. Die Logger-Unit hat eine Funktion, die man aus dem
Timerinterrupt zyklisch aufrufen muss. Mit loggerStart(); (was
typischerweise aus dem UART-Interrupt aufgerufen wird) wird dann die
Aufzeichnung gestartet, d.h. die Variable, die mit dem Pointer
referenziert wurde, wird bei jedem Timeraufruf ausgelesen und in das
Datenarray abgespeichert. Ist der Buffer voll, dann werden die Daten zur
Anzeige an den PC geschickt.
Die Implementierung ist so, dass in den Anfang des Buffers die Daten für
die Kanalverwaltung geschrieben werden, das ist ein Struct-Array mit
Struct für jeden Kanal.
In dem Struct steht 1. der Pointer auf die Zielvariable, 2. die Adresse,
ab der im Buffer die Daten für diesen Kanal anfangen, 3. ein
Funktionspointer auf die Loggingfunktion für diesen Datentyp (ein
Bool-Wert muss ja anders gelesen und abgespeichert werden, wie ein
32bit-Wert oder ein 8bit-Wert), und 4. eine Hilfsvariable in die
Bool-Werte reingeshiftet werden und die beim Auslesen als
Funktionspointer für die verschiedenen Typen verwendet wird.
Hinter dem Structarray fängt dann der Datenbereich für die ganzen Kanäle
an.
Zum Loggen wird in der Timerfunktion das Kanalarray durchlaufen und für
jeden Kanal der typspezifische Loggingfunktionspointer aufgerufen.
Also vom Prinzip her nicht übertrieben kompliziert, bzw. wüsste ich
nicht, wie ich es einfacher machen könnte.
Ein Anfänger schrieb:> Wie gesagt, finden die Zugriffe in meinem Fall sowieso flaggeschützt> streng nacheinander statt, meine einzige Sorge ist also das> Wegoptimieren.
Dann muss das Flag ein _Atomic bool bzw. std::atomic<bool> sein (oder
der entsprechende Typ-Alias), und natürlich muss Dein Algorithmus damit
den nebenläufigen Zugriff auf die gemeinsamen Datenstrukturen
verhindern. Und wie oben schon gesagt: `volatile` ist falsch.
Wenn Du alles richtig machst, brauchst Du Dir über das Optimieren keine
sorgen zu machen ;-) (s.a.
Beitrag "Re: c volatile -> wann bracht mans wirklich?")
Wilhelm M. schrieb:> Und wie oben schon gesagt: `volatile` ist falsch.
Sind wir schon bei 10 Seiten?
Εrnst B. schrieb:> Es ist einfacher, einem Anfänger ein "dann schreib halt volatile davor"> hinzuwerfen, anstatt ihm einen 10-Seiten-Aufsatz über die Freiheiten,> die der C-Standard dem Compiler/Optimizer gewährt, und wie man damit> umgeht, zu schreiben. Den versteht er dann eh nicht.
Wilhelm M. schrieb:> Und so schwierig ist es ja auch nicht
Schwierig ist es, gegen die zehntausenden Stunden Video anzukommen, die
Youtube nur für "Arduino volatile" ausspuckt.
Es ist vermutlich einfacher, das C-Std-Komitee davon zu überzeugen die
Bedeutung von "volatile" zu ändern, als den Anfängern und
Arduino-Jüngern ihr Lieblings-Keyword auszutreiben.
Siehst ja, was das ganze Hinreden im Endeffekt beim TE bewirkt hat...
Er bleibt bei "volatile", weil das portabel und bei allen denkbaren
C-Compilern gleichermaßen falsch ist.
Εrnst B. schrieb:> Siehst ja, was das ganze Hinreden im Endeffekt beim TE bewirkt hat...> Er bleibt bei "volatile", weil das portabel und bei allen denkbaren> C-Compilern gleichermaßen falsch ist.
... und er hat vermutlich das `_Atomic` bei seinem flag vergessen ;-)
Εrnst B. schrieb:> Er bleibt bei "volatile", weil das portabel und bei allen denkbaren> C-Compilern gleichermaßen falsch ist.
Mancher bleibt auch bei volatile, weil _Atomic erst ab C11 verfügbar
ist, und es da erst mal C99 in den produktiven Einsatz schaffen muß.
Oliver
Wilhelm M. schrieb:> Und wie oben schon gesagt: `volatile` ist falsch.
Nur gab es für den AVR-GCC lange Zeit nichts anderes. Die atomic.h ist
erst später hinzugekommen und für Memory-Barrier gibt es gar keine
anwenderfreundliche Lib.
Die atomic.h ist außerdem falsch, wenn z.B. für eine UART-FIFO kein
atomarer Zugriff nötig ist, weil Puffer <256Byte. Da reicht die
Nichtoptimierung in der Main völlig aus. Ich hab mir dafür ein Macro
geschrieben, was einen Zugriff nach volatile castet:
1
#define IVAR(x) (*(volatile typeof(x)*)&(x))
Ich wäre auch bereit, ein gleichwertiges Memory-Barrier Macro zu
benutzen.
Oliver S. schrieb:> Εrnst B. schrieb:>> Er bleibt bei "volatile", weil das portabel und bei allen denkbaren>> C-Compilern gleichermaßen falsch ist.>> Mancher bleibt auch bei volatile, weil _Atomic erst ab C11 verfügbar> ist, und es da erst mal C99 in den produktiven Einsatz schaffen muß.
Das Problem ist nur, dass `volatile` und `atomic` zwei unterschiedliche
Dinge sind.
Peter D. schrieb:> Wilhelm M. schrieb:>> Und wie oben schon gesagt: `volatile` ist falsch.>> Nur gab es für den AVR-GCC lange Zeit nichts anderes. Die atomic.h ist> erst später hinzugekommen und für Memory-Barrier gibt es gar keine> anwenderfreundliche Lib.
Mag sein: trotzdem ist 'volatile' an der Stelle falsch.
Wenn es kein Macro für die MemBarrier gab, dass muss man es eben zu Fuss
hinschreiben. sei()/cli() tun das ja im übrigen.
>> Die atomic.h ist außerdem falsch, wenn z.B. für eine UART-FIFO kein> atomarer Zugriff nötig ist, weil Puffer <256Byte. Da reicht die> Nichtoptimierung in der Main völlig aus. Ich hab mir dafür ein Macro> geschrieben, was einen Zugriff nach volatile castet:>
1
>#defineIVAR(x)(*(volatiletypeof(x)*)&(x))
2
>
>> Ich wäre auch bereit, ein gleichwertiges Memory-Barrier Macro zu> benutzen.
s.a. _MemoryBarrier()
Wilhelm M. schrieb:> s.a. _MemoryBarrier()
Schön für Dich, daß wenigstens Du weißt, wie diese Funktion definiert
ist.
Bei mir gibt das eine Fehlermeldung.
Wilhelm M. schrieb:> Das Problem ist nur, dass `volatile` und `atomic` zwei unterschiedliche> Dinge sind.
Das ist so. Es gibt ja noch viel mehr falsche Anwendungen für volatile,
die letzendlich u.a. das C++-Standard-Komite auf die Idee gebracht
haben, das mehr oder weniger ganz abzuschaffen.
Wilhelm M. schrieb:> Wenn es kein Macro für die MemBarrier gab, dass muss man es eben zu Fuss> hinschreiben.
So ist es.
Oliver
Peter D. schrieb:> Wilhelm M. schrieb:>> s.a. _MemoryBarrier()>> Schön für Dich, daß wenigstens Du weißt, wie diese Funktion definiert> ist.
Du weißt bestimmt auch, dass das kein Funktion ist (sein kann). Es ist
ein Macro.
> Bei mir gibt das eine Fehlermeldung.
Dann hast Du das include vergessen:
#include <avr/cpufunc.h>
Ein Anfänger schrieb:> Ich verstehe ehrlich gesagt nicht,> warum c nicht auch ein Unit-System verwendet, ...
Weil man es seinerzeit nicht so vorgesehen hatte, aus welchen Gründen
auch immer.
Eine grundlegende Reform des des C-Dialektes hat eigentlich erst mit C#
statt gefunden, wenn auch an vielen Stellen nur halbherzig. Es gäbe
einiges was man in C mal grundlegend aufräumen sollte, um einige
Stolpersteine mal zu eliminieren. Allerdings schein diesbezüglich der
Schmerz nicht groß genug zu sein und die C-Programmierer haben sich halt
mit den Tücken arrangiert und leben damit.
Oliver S. schrieb:> Auch wenn er es nicht kann, braucht> es kein volatile, solange die Hardware es nicht erfordert.
Das hat mit der Hardware nun mal rein gar nichts zu tun.
Das volatile verhindert, einfach ausgedrückt, schlichtweg das der
Compiler eine Variable einfach wegoptimiert, von der er meint das sie
überflüssig sei - aus welchen Gründen auch immer.
Der Oliver lese mal hier
https://de.wikipedia.org/wiki/Volatile_(Informatik) nach. Dort sind auch
zwei kleine Beispiele aufgeführt, was ein Weglassen von volatile
bewirken könnte. Mit HW hat das aber alles nichts zu tun.
Zeno schrieb:> Das volatile verhindert, einfach ausgedrückt, schlichtweg das der> Compiler eine Variable einfach wegoptimiert, von der er meint das sie> überflüssig sei - aus welchen Gründen auch immer.
Mit wegoptimieren hat das nichts zu tun.
Der Wortlaut aus dem Standard hilft da, wie immer, am besten weiter:
1
"An object that has volatile-qualified type may be modified in ways unknown to the
2
implementation or have other unknown side effects. Therefore any expression referring
3
to such an object shall be evaluated strictly according to the rules of the abstract machine,
4
as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the
5
object shall agree with that prescribed by the abstract machine, except as modified by the
6
unknown factors mentioned previously.134) What constitutes an access to an object that
7
has volatile-qualified type is implementation-defined."
Zeno schrieb:> Das hat mit der Hardware nun mal rein gar nichts zu tun.
Doch, hat es praktisch immer. "in ways unknown to the
implementation" passiert eigentlich nur bei durch die Hardware
hervorgerufene Effekte (wie Zählerregister, Multithreading, ISRs, ...)
Oliver
Zeno schrieb:> https://de.wikipedia.org/wiki/Volatile_(Informatik) nach. Dort sind auch> zwei kleine Beispiele aufgeführt, was ein Weglassen von volatile> bewirken könnte. Mit HW hat das aber alles nichts zu tun.
Sehr schlechter Wikipediaartikel. Das erste Beispiel ist direkt
Undefined Behaviour. Volatile führt nicht zu einer Memory Barrier. Je
nach Architektur wird diese aber für Cachesynchronisation benötigt.
Oliver S. schrieb:> Das ist so. Es gibt ja noch viel mehr falsche Anwendungen für volatile,> die letzendlich u.a. das C++-Standard-Komite auf die Idee gebracht> haben, das mehr oder weniger ganz abzuschaffen.
Deswegen wird langfristig wohl auch was bei Arduino geschehen.
Read-Modify-Write Ausdrücke sind Deprecated und geben eine entsprechende
Warnung zurück (https://godbolt.org/z/TKq4rqzjY) In ein paar Jahren gibt
es Fehlermeldungen und man ist gezwungen einen älteren C++-Standard zu
verwenden oder eben Atomics zu nutzen.
avr schrieb:> Deswegen wird langfristig wohl auch was bei Arduino geschehen.
Das Standardkomitee ist ja wohl doch erst mal zurückgerudert, und denkt
nochmals drüber nach. Mal schauen, was draus wird.
Oliver
avr schrieb:> Zeno schrieb:>> https://de.wikipedia.org/wiki/Volatile_(Informatik) nach. Dort sind auch>> zwei kleine Beispiele aufgeführt, was ein Weglassen von volatile>> bewirken könnte. Mit HW hat das aber alles nichts zu tun.>> Sehr schlechter Wikipediaartikel. Das erste Beispiel ist direkt> Undefined Behaviour. Volatile führt nicht zu einer Memory Barrier. Je> nach Architektur wird diese aber für Cachesynchronisation benötigt.
Der Artikel ist zwar schlecht, weil er den Kern nicht erfasst.
Aber das erste Beispiel ist kein UB, so wie es da steht. Einfach weil da
kein nebenläufiger Zugriff stattfindet bzw. erkennbar ist.
Wilhelm M. schrieb:> Aber das erste Beispiel ist kein UB, so wie es da steht. Einfach weil da> kein nebenläufiger Zugriff stattfindet bzw. erkennbar ist.
Doch, der wird ja im Text beschrieben. Es soll keine Endlosschleife
erzeugt werden, weil [aus einem anderen Kontext] status geändert werden
könnte. Und da keine atomics verwendet werden, ist da ganz klar eine
Race condition aka UB.
avr schrieb:> Wilhelm M. schrieb:>> Aber das erste Beispiel ist kein UB, so wie es da steht. Einfach weil da>> kein nebenläufiger Zugriff stattfindet bzw. erkennbar ist.>> Doch, der wird ja im Text beschrieben.
Sehe ich nicht. Welchen Satz meinst Du? Wo?
M.E. will der Artikel nur zeigen, dass in diesem Fall die Funktion nicht
zu einem Noop verkürzt wird. Letztlich genau das, was mein bei
HW-Registern braucht ...
Wilhelm M. schrieb:> Εrnst B. schrieb:>>> Siehst ja, was das ganze Hinreden im Endeffekt beim TE bewirkt hat...>> Er bleibt bei "volatile", weil das portabel und bei allen denkbaren>> C-Compilern gleichermaßen falsch ist.>> ... und er hat vermutlich das `_Atomic` bei seinem flag vergessen ;-)
Ist denn c da nicht rückwärtskompatibel? Also ein Code mit volatile Flag
geschrieben vor Einführung des _Atomic wird nach Einführung plötzlich
speicheroptimiert mit anderen Variablen in eine Speicherstelle
geschrieben und damit nicht mehr atomar? Ich denke, _Atomic ist auf
einem höheren Abstraktionslevel angesiedelt, nämlich je nach
Hardwareunterstützung kann der Compiler auf passende "Datentypen"
zurückgreifen, spezielle Assembler-Befehle verwenden, oder muss
Interruts deaktivieren. Zusätzlich muss er die Aufrufe wie auch bei
volatile in seinen Optimierungen berücksichtigen. Bei Mehrkernen mit
Cache muss er je ggf. auch noch Hardware-Memory-Barrieren mit
Cache-Flushes einfügen.
Volatile dagegen scheint nur die Anweisung an den Compiler zu sein:
Lade/Schreibe wie es da steht. Bei bekannter
Einkernmikrocontrollerarchitektur sollte das ja reichen.
Εrnst B. schrieb:> Siehst ja, was das ganze Hinreden im Endeffekt beim TE bewirkt hat...> Er bleibt bei "volatile", weil das portabel und bei allen denkbaren> C-Compilern gleichermaßen falsch ist.
Mit _Atomic könnte ich mich schon anfreunden, Kompatibilität ist mir
aber wichtig, weil ich den Code wenn er fertig ist auch ins Netz stellen
will. Wie ist denn die Verbreitung von C11? Verwenden die aktuellen
Compiler c11? Beim gcc hab ich gerade geschaut, da ist das wohl so.
Allerdings unterstützt _Atomic wohl keine Structs. Naja, wenn mans bei
Pointern sowieso nicht braucht...
Wilhelm M. schrieb:> Dann hast Du das include vergessen:>> #include <avr/cpufunc.h>
Naja, eigentlich gehört es sich, die nötigen Include mit zu nennen und
nicht nur die nackte Funktion dem Leser vor den Latz zu knallen.
Wenn ich das richtig verstehe, ist das aber nicht gleichbedeutend mit
einem volatile cast einer Variablen. Es werden alle Variablen in dem
Kontext verworfen, also nicht nur die den Interrupt betreffende. Die
Memory Barrier kann also einen deutlich höheren Codeverbrauch bewirken.
Oliver S. schrieb:> Zeno schrieb:>> Das volatile verhindert, einfach ausgedrückt, schlichtweg das der>> Compiler eine Variable einfach wegoptimiert, von der er meint das sie>> überflüssig sei - aus welchen Gründen auch immer.>> Mit wegoptimieren hat das nichts zu tun.
Na ja, ganz nichts glaub ich auch wieder nicht. Es ist ja auf jeden Fall
so, dass Schreiben auf volatile-Variablen billig, auslesen teuer ist
(teuer im Sinne von expliziter Cast notwendig).
Und das ist glaube ich die ganze Magie dahinter. Was die Optimierung
betrifft geht es ja meist eher um solche Konstrukte, wo der Compiler
halt irgendne Heuristik drauf anwendet, um den Code schneller zu machen.
Wenn du halt ne Initialbelegung mit 0 für ne nicht-volatile Variable
hast und die in nem Schleifenkopf ausführst, könnte z.B. der GCC bei -O3
eventuell auf die Idee kommen, dass die Schleife gar nicht ausgeführt zu
werden braucht. Siehe oben erwähnten Artikel das setjmp-Beispiel:
https://de.wikipedia.org/wiki/Volatile_(Informatik)
Dort ist, soweit ich's versteh, die Notwendigkeit für volatile daher
gegeben, weil sich durch das Asm-ähnliche Gejumpe eben außerhalb des
Compilermodells für C bewegt wird. Soll heißen: der C-Compiler kann
nicht wissen, was da 'schönes' gemacht wird. Ähnlich wirds bei
inline-Assembly sein. Wenn der Compiler das nicht sehen kann, weils halt
nicht in sein eigenes Modell für Ausführungen passt - dann ist wohl die
Notwendigkeit für volatile auch mit gegeben.
> Der Wortlaut aus dem Standard hilft da, wie immer, am besten weiter:> "An object that has volatile-qualified type may be modified in ways> unknown to the> implementation or have other unknown side effects. Therefore any> expression referring> to such an object shall be evaluated strictly according to the rules of> the abstract machine,> as described in 5.1.2.3. Furthermore, at every sequence point the value> last stored in the> object shall agree with that prescribed by the abstract machine, except> as modified by the> unknown factors mentioned previously.134) What constitutes an access to> an object that> has volatile-qualified type is implementation-defined."> Zeno schrieb:>> Das hat mit der Hardware nun mal rein gar nichts zu tun.>> Doch, hat es praktisch immer. "in ways unknown to the> implementation" passiert eigentlich nur bei durch die Hardware> hervorgerufene Effekte (wie Zählerregister, Multithreading, ISRs, ...)
Na ja, wie beschrieben, eigenes inline-Assembly ist auch SW-seitige
Programmsteuerung, wo die Hardware erstmal nix für kann - und dennoch
isses außerhalb des C-Sprachmodells.
Peter D. schrieb:> Wilhelm M. schrieb:>> Dann hast Du das include vergessen:>>>> #include <avr/cpufunc.h>>> Naja, eigentlich gehört es sich, die nötigen Include mit zu nennen und> nicht nur die nackte Funktion dem Leser vor den Latz zu knallen.
Ach Peter, Du kennst Dich doch ganz gut aus, deswegen ...
> Wenn ich das richtig verstehe, ist das aber nicht gleichbedeutend mit> einem volatile cast einer Variablen.
Genau!
> Es werden alle Variablen in dem> Kontext verworfen, also nicht nur die den Interrupt betreffende.
Es werden alle Load/Stores von Objekten materialisiert, der Adresse aus
dem Block entweichen können (s.a. escape analysis eines Compilers).
Typischerweise in diesem Anwendungsfall also genau die globalen
Variablen, die in ISR und e.g. main() verwendet werden.
avr schrieb:> Sehr schlechter Wikipediaartikel. Das erste Beispiel ist direkt> Undefined Behaviour. Volatile führt nicht zu einer Memory Barrier. Je> nach Architektur wird diese aber für Cachesynchronisation benötigt.avr schrieb:> Und da keine atomics verwendet werden, ist da ganz klar eine> Race condition aka UB.
Die Architektur kann dem Programmierer doch bekannt sein!? Bei
Einkernern ist dann auch der Cache egal. Und selbst bei Mehrkernern mit
Cache gäbe es keine Race Condition, weil der zeitliche Ablauf gewahrt
ist und man von regelmäßigen automatischen Cache-Synchronisationen
ausgehen kann.
db8fs schrieb:> Na ja, ganz nichts glaub ich auch wieder nicht. Es ist ja auf jeden Fall> so, dass Schreiben auf volatile-Variablen billig, auslesen teuer ist> (teuer im Sinne von expliziter Cast notwendig).
Warum das?
db8fs schrieb:> Soll heißen: der C-Compiler kann> nicht wissen, was da 'schönes' gemacht wird.
Genau das ist alles, was dahintersteckt. Und daher:
"such an object shall be evaluated strictly according to the rules of
the abstract machine".
Nicht mehr und nicht weniger.
db8fs schrieb:> Na ja, wie beschrieben, eigenes inline-Assembly ist auch SW-seitige> Programmsteuerung, wo die Hardware erstmal nix für kann - und dennoch> isses außerhalb des C-Sprachmodells.
Das stimmt natürlich.
Oliver
Ein Anfänger schrieb:> Ist denn c da nicht rückwärtskompatibel? Also ein Code mit volatile Flag> geschrieben vor Einführung des _Atomic wird nach Einführung plötzlich> speicheroptimiert mit anderen Variablen in eine Speicherstelle> geschrieben und damit nicht mehr atomar?
Hast Du
Beitrag "Re: c volatile -> wann bracht mans wirklich?"
gelesen?
`volatile` und `atomar` sind zwei orthogonale Konzepte, d.h. sie haben
nichts miteinander zu tun.
Ein Anfänger schrieb:> Volatile dagegen scheint nur die Anweisung an den Compiler zu sein:> Lade/Schreibe wie es da steht.
Ja, im wesentlichen.
> Bei bekannter> Einkernmikrocontrollerarchitektur sollte das ja reichen.
Garantiert Dir aber kein Atomarität, liefert bei konkurrierendem Zugriff
trotzdem UB, und verhindert mögliche Optimierungen, die sonst (bei
normalen Speicherzellen, kein HW-Register) sinnvoll wären.
Wilhelm M. schrieb:> Ein Anfänger schrieb:>> Ist denn c da nicht rückwärtskompatibel? Also ein Code mit volatile Flag>> geschrieben vor Einführung des _Atomic wird nach Einführung plötzlich>> speicheroptimiert mit anderen Variablen in eine Speicherstelle>> geschrieben und damit nicht mehr atomar?>> Hast Du>> Beitrag "Re: c volatile -> wann bracht mans wirklich?">> gelesen?>> `volatile` und `atomar` sind zwei orthogonale Konzepte, d.h. sie haben> nichts miteinander zu tun.
Auch 'atomar' geht doch von Nebenläufigkeit aus, sonst bräuchte man den
atomaren Zugriff ja gar nicht. Und vor 'atomar' konnte man nach Standard
scheinbar nur auf 'volatile' in Kombination mit anderen Konzepten (z.B.
target-passenden Datentypen) zurückgreifen, um atomaren garantierten
Zugriff zu gewährleisten. Wenn Rückwärtskompatibilität zu altem Code
gegeben sein soll, müsste also volatile für ein Flag weiterhin reichen?
(Ja, hab ich gelesen.)
Ein Anfänger schrieb:> Auch 'atomar' geht doch von Nebenläufigkeit aus, sonst bräuchte man den> atomaren Zugriff ja gar nicht.
Na klar.
Nur das "auch" ist falsch: `volatile` hat nichts mit Nebenläufigkeit zu
tun und erfüllt auch keine Garantien dafür.
> Und vor 'atomar' konnte man nach Standard> scheinbar nur auf 'volatile' in Kombination mit anderen Konzepten (z.B.> target-passenden Datentypen) zurückgreifen, um atomaren garantierten> Zugriff zu gewährleisten. Wenn Rückwärtskompatibilität zu altem Code> gegeben sein soll, müsste also volatile für ein Flag weiterhin reichen?
Wenn kein _Atomic oder std::atomic<> verfügbar ist, dann (wie oben
beschrieben) explizites Herstellen einer happens-before Relation, also
hier Abschalten der Nebenläufigkeit zusammen mit Memory-Barrier
(Compiler und ggf. CPU).
Wilhelm M. schrieb:> Sehe ich nicht. Welchen Satz meinst Du? Wo?
der Wert der Variable jederzeit ohne expliziten Zugriff im Quelltext
ändern kann, etwa durch externe Hardware oder **asynchron ausgeführte
ISRs**
Letzeres ist Nebenläufigkeit. Wenn bei der Variable explizit stehen
würde, dass sie ausschließlich für ein Statusregister stehen soll, wäre
ich bei dir.
Ein Anfänger schrieb:> Die Architektur kann dem Programmierer doch bekannt sein!? Bei> Einkernern ist dann auch der Cache egal. Und selbst bei Mehrkernern mit> Cache gäbe es keine Race Condition, weil der zeitliche Ablauf gewahrt> ist und man von regelmäßigen automatischen Cache-Synchronisationen> ausgehen kann.
Das ist nicht der Gedanke von Hochsprachen. Die Architektur soll dem
Programmierer in den meisten Fällen gar nicht interessieren. Man möchte
portablen Code schreiben, der auch noch funktioniert, wenn man die
Architektur wechselt. Darum hält man sich sinnvollerweise auch an den
Standard, der garantiert, dass der Code dann auch noch funktioniert.
Von regelmäßigen Cache-Sychronisationen kann man übrigens nicht
ausgehen. Ich könnte eine Architektur entwerfen, die dies nur explizit
macht. Das wäre aus Sicht des Standards vollkommen in Ordnung. Ich werde
so eine Architektur nicht entwerfen, aber wer garantiert dir, dass das
nicht irgendjemand in Zukunft macht? Code, der sich nicht an den
Standard hält, ist dann schlicht nicht portabel.
Nebenbei, Undefined Behaviour bedeutet, dass der Compiler alles machen
darf. Alles heißt, wenn es im Controller ein Selbstzerstörungsbit gäbe,
dürfte der Compiler dies bei Undefined Bahaviour setzen. Er dürfte den
kompletten Ram löschen usw.
Du kannst natürlich sagen, auf meiner Architektur mit diesem Compiler
scheint es trotzdem zu funktionieren. Aber es bleibt eben ein
Glücksspiel mit jedem Architektur und Compilerwechsel. Es wird meist
funktionieren, aber ist das ein Grund auf die einfachen in C11 und C++11
verfügbaren Mittel zu verzichten, die es garantieren würden?
avr schrieb:> Wilhelm M. schrieb:>> Sehe ich nicht. Welchen Satz meinst Du? Wo?>> der Wert der Variable jederzeit ohne expliziten Zugriff im Quelltext> ändern kann, etwa durch externe Hardware oder **asynchron ausgeführte> ISRs**
Mag sein, dass der Autor das gemeint hat ...
Der Quelltext allein in dem Beispiel ist aber noch kein UB. Hier fehlt
schlicht die Deklaration einer ISR (bspw. als Presudo-Code), die das
erkennen ließe.
avr schrieb:> Ein Anfänger schrieb:>> Die Architektur kann dem Programmierer doch bekannt sein!? Bei>> Einkernern ist dann auch der Cache egal. Und selbst bei Mehrkernern mit>> Cache gäbe es keine Race Condition, weil der zeitliche Ablauf gewahrt>> ist und man von regelmäßigen automatischen Cache-Synchronisationen>> ausgehen kann.>> Das ist nicht der Gedanke von Hochsprachen. Die Architektur soll dem> Programmierer in den meisten Fällen gar nicht interessieren.
Ja, stimmt. Nur kann man sich durch global deaktivierte Interrupts (je
nach Datentyp/Zugriff) auch Probleme einfangen, mit denen man bei einem
einfachen atomic gar nicht gerechnet hat. c ist ja eine
Low-Level-Sprache, da könnte es ja noch was 'hardwarenäheres' geben.
> Nebenbei, Undefined Behaviour bedeutet, dass der Compiler alles machen> darf.
Ja, das versteh ich. War es auch vor c11 schon undefined behaviour?
Grundsätzlich finde ich diese Diskussion recht interessant.
Ich stelle mir jedoch folgende Frage: Ist volatile (einer einzelnen
globalen Variablen) nicht unter Umständen deutlich performanter als der
Einsatz einer Memory-Barrier?
Bei ›volatile‹ wird ja nur genau diese eine Variable frisch gelesen.
Bei Einsatz einer Memory-Barrier müssen ja alle Variablen welche in
CPU-Registern ›gecacht‹ sind, neu geladen werden. Gerade bei Prozessoren
mit einem ganzen Sack voller Register eher unschön und bremsend.
Oder habe ich da irgendwo einen Gedankenfehler?
Norbert schrieb:> Grundsätzlich finde ich diese Diskussion recht interessant.>> Ich stelle mir jedoch folgende Frage: Ist volatile (einer einzelnen> globalen Variablen) nicht unter Umständen deutlich performanter als der> Einsatz einer Memory-Barrier?
Nein. Meistens ist die Memory-Barrier besser, weil damit noch
Optimierungen möglich sind, bei `volatile` aber jeder Zugriff (auch
Lesen) immer ausgeführt werden muss, weil der Compiler die Werte des
Objektes nicht als stabil ansehen kann.
> Bei ›volatile‹ wird ja nur genau diese eine Variable frisch gelesen.> Bei Einsatz einer Memory-Barrier müssen ja alle Variablen welche in> CPU-Registern ›gecacht‹ sind, neu geladen werden.
Nein.
Die Memory-Barrier erzwingt nur ein load / store eines Objektes, dessen
Adresse den lokalen Block verlassen haben könnte. Sie kommt damit dem
Aufruf einer non-inline Funktion gleich (s.a. escape analysis, hatte ich
oben schon erwähnt).
Oliver S. schrieb:> db8fs schrieb:>> Na ja, ganz nichts glaub ich auch wieder nicht. Es ist ja auf jeden Fall>> so, dass Schreiben auf volatile-Variablen billig, auslesen teuer ist>> (teuer im Sinne von expliziter Cast notwendig).>> Warum das?
Weil die Werte bei volatile eben nicht stabil sind. Du darfst volatile
Variablen immer beschreiben, aber grundsätzlich nicht ohne cast einer
nicht-volatile Variablen zuweisen. Teuer ist das in dem Sinne, dass dein
Programm damit Nichtdeterminismus zulässt, d.h. es ist in einem
besonderen Maße von externen Umständen abhängig. Deswegen auch der cast,
weil damit der Programmierer ja markieren soll, dass er weiß, was er
damit tut.
Mal paar Beispiele von Abhängigkeiten, von lose gekoppelt zu fester
gedongelt:
1
// zustandsfreie Funktion (reentrant)
2
int add(int a, int b)
3
{
4
return a+b;
5
}
6
7
// zustandsbehafte Funktion (nicht-reentrant)
8
int accumulate(int a)
9
{
10
static int accumulator=0;
11
accumulator += a;
12
return accumulator;
13
}
Abstrakt gesehen verhält sich dein volatile ähnlich wie die accumulate:
2 Aufrufe mit den selben Parametern !=0 kommen unterschiedliche
Ergebnisse zurück. Ist aber immer noch testbar und deterministisch.
Das Lesen von volatile ist zustandsbehaftet und nichtdeterministisch -
also gleich 2 Sachen, die man in der Regel nicht unbedingt in gutem Code
haben will und zur Laufzeit potentiell problematisch sein können.
Norbert schrieb:> Ich stelle mir jedoch folgende Frage: Ist volatile (einer einzelnen> globalen Variablen) nicht unter Umständen deutlich performanter als der> Einsatz einer Memory-Barrier?
Nein, keine Barrieren (lock-free-programming) ist immer performanter als
auf irgendwas warten zu müssen. volatile kann das aber nicht leisten.
Ein Anfänger schrieb:> Ja, stimmt. Nur kann man sich durch global deaktivierte Interrupts (je> nach Datentyp/Zugriff) auch Probleme einfangen, mit denen man bei einem> einfachen atomic gar nicht gerechnet hat. c ist ja eine> Low-Level-Sprache, da könnte es ja noch was 'hardwarenäheres' geben.
Ein atomic heißt nicht direkt, dass Interrupts Global deaktiviert
werden. Das hängt tatsächlich stark von der Architektur ab und welche
Möglichkeiten der Compiler hat. Aber da bewegen wir uns schon in den
Bereich der Mikrooptimierung. Wenn das tatsächlich notwendig ist, kann
man durchaus schauen, mit welchen Mitteln man mehr Performanz erreicht,
z.B auch mit Inline assembly, aber man sollte sich immer bewusst sein,
was man tut. In den meisten Fällen kommt es nicht auf die letzte
Mikrosekunde an und da schreibe ich ich lieber standardkonformen,
portablen Code. Der ist wiederverwendbar, ich kann ihn auch am PC testen
und verbringe weniger Zeit mit Debugging.
Als Beispiel: in der ARM Architektur gibt es spezielle Befehle für
atomare read modify write Zugriffe und der Compiler nutzt diese auch in
Kombination mit atomics. Wenn man dagegen nur auf ein atomic schreibt
und dies mit einem Befehl abbildbar ist, dann wird das auch vom Compiler
so übersetzt. Der einzige Unterschied zu volatile sind in dem Fall die
zusätzlichen memory barriers, die der Compiler zusätzlich generiert.
>> Nebenbei, Undefined Behaviour bedeutet, dass der Compiler alles machen>> darf.> Ja, das versteh ich. War es auch vor c11 schon undefined behaviour?
Volatile war nie für Nebenläufigkeit gedacht, also ja. Ich meine aber in
C gab es schon vorher pthreads, mit denen man korrekt programmieren
konnte. In C++ war das dagegen ein Problem, denn es gab vor C++11 kein
Speichermodell und multithreading war somit allgemein problematisch.
avr schrieb:>>> Nebenbei, Undefined Behaviour bedeutet, dass der Compiler alles machen>>> darf.>> Ja, das versteh ich. War es auch vor c11 schon undefined behaviour?>> Volatile war nie für Nebenläufigkeit gedacht, also ja. Ich meine aber in> C gab es schon vorher pthreads, mit denen man korrekt programmieren> konnte. In C++ war das dagegen ein Problem, denn es gab vor C++11 kein> Speichermodell und multithreading war somit allgemein problematisch.
Noch mal zum Undefined Behaviour, wo ist denn das undefinierte Verhalten
definiert? (Unabhängig davon, dass atomic für ein Flag besser geeignet
ist.)
Wie hier schon mal gepostet wurde heißt es zu volatile im Standard:
"Therefore any expression referring to such an object shall be evaluated
strictly according to the rules of the abstract machine, as described in
5.1.2.3."
und in 5.1.2.3.
"In the abstract machine, all expressions are evaluated as specified by
the semantics."
Ich versteh das so, dass der Compiler genau das tun muss, was im Code
dasteht, d.h. mit dem gegebenen Datentyp (!) in die Speicherstelle
schreiben, bzw. an anderer Codestelle diese auslesen. Und dann ist es
doch egal, ob das ein Interruptflagregister ist, dass sich
hardwareseitig verändern kann oder ob es sich um eine Speicherstelle im
RAM handelt. Selbst wenn der Compiler den volatile-'Missbrauch' erkennen
würde, müsste er gemäß Spezifikation die Speicherstelle auslesen, bzw.
an der anderen Stelle schreiben. Und so wäre es auch wenn es sprachlich
im Hinblick auf die Atomarität nicht definiert ist, im Hinblick auf den
Speicherzugriff doch wohl definiert und mit Kenntnis der Hardware dann
ein legitimes Konstrukt?
avr schrieb:> Ein Anfänger schrieb:>> Ja, stimmt. Nur kann man sich durch global deaktivierte Interrupts (je>> nach Datentyp/Zugriff) auch Probleme einfangen, mit denen man bei einem>> einfachen atomic gar nicht gerechnet hat. c ist ja eine>> Low-Level-Sprache, da könnte es ja noch was 'hardwarenäheres' geben.>> Ein atomic heißt nicht direkt, dass Interrupts Global deaktiviert> werden. Das hängt tatsächlich stark von der Architektur ab und welche> Möglichkeiten der Compiler hat. Aber da bewegen wir uns schon in den> Bereich der Mikrooptimierung.
Mir ist klar, dass der Compiler nutzt was er kann. Ich meinte nur, dass
die Verwendung von atomic ein Rattenschwanz hinter sich herschleifen
kann (kann), mit der der Programmierer gar nicht rechnet. Wie bspw. die
Deaktivierug von Interrupts (wenn nötig) und plötzlich stimmen die
Zeiten einer Port-Messung nicht mehr.
(Ich sprech hier immer vom Einsatz auf einem Mikrocontroller, dass auf
dem PC Vieles anders ist, ist klar.)
Ein Anfänger schrieb:> Ich versteh das so, dass der Compiler genau das tun muss, was im Code> dasteht, d.h. mit dem gegebenen Datentyp (!) in die Speicherstelle> schreiben, bzw. an anderer Codestelle diese auslesen.> Und dann ist es> doch egal, ob das ein Interruptflagregister ist, dass sich> hardwareseitig verändern kann oder ob es sich um eine Speicherstelle im> RAM handelt. Selbst wenn der Compiler den volatile-'Missbrauch' erkennen> würde, müsste er gemäß Spezifikation die Speicherstelle auslesen, bzw.> an der anderen Stelle schreiben.
Im Prinzip ja, es ist egal - es ist allerdings auch unnötig bei normalen
Speicheraddressen und wäre daher zuviel des Guten (DOWN - do only what's
necessary).
Denn wenn du an der Stelle auf die volatile-Semantik bestehst,
überdeckst du ja, dass es auch ohne funktionieren würde. Jemand, der
deinen Code liest - im Zweifelsfall du selber nach x Monaten - könnte
dann schon dezent stutzig werden und sich leise 'wtf' fragen. Weil
eigentlich willste ja keine Seiteneffekte im Code haben und
Missverständlichkeiten sind auch doof, die kriegste selbst bei sauberen
Code schon teilweise aufgrund blöder Benamung, ungünstiger
Strukturierung oder Einarbeitungsstand. Daher, immer besser weglassen
bis auf den Fall der direkten HW-Interaktion.
Btw. grundsätzlich hab ich gegen volatile gar nicht so sehr viel. Ich
glaub es gab mal nen Alexandrescu-Artikel drüber, wo der das -
allerdings für C++ - quasi wie const verwendet hat, um damit
multithreaded Locks zu machen. Ist aber schon "hornbeinalt" der Artikel,
da gabs noch keinen scoped_lock, keinen std::mutex usw.
Man konnte da volatile-Variablen von ner Klasse anlegen, die waren dann
unsynchronisiert. Hat man eine normale Instanz der Klasse gebildet (ohne
volatile), hat die vorher nen Mutex gelocked und unter dem dann die als
volatile qualifizierten Methoden aufgerufen. Sollte wohl ne Art
Compile-Time-Erkennung für RaceConditions sein.
Stand glaube bei DrDobbs mal drin.
Ein Anfänger schrieb:> Nur kann man sich durch global deaktivierte Interrupts (je> nach Datentyp/Zugriff) auch Probleme einfangen, mit denen man bei einem> einfachen atomic gar nicht gerechnet hat.
Eine globale Sperre garantiert die geringste Zeit der Sperre und hat
daher die geringsten Nebenwirkungen.
Ganz anders dagegen, wenn man nur den betroffenen Interrupt sperrt. Dann
kann der langsamste Interrupt das Timing vollkommen durcheinander
bringen und sogar eine Prioritätsinversion erfolgen. Die Nebenwirkungen
sind daher erheblich und nicht mehr überschaubar.
Einige ARM haben einen recht eleganten Weg der globalen Sperre, man kann
temporär den Level der aktuellen Task auf den höchsten Wert setzen.
Ein Anfänger schrieb:> Noch mal zum Undefined Behaviour, wo ist denn das undefinierte Verhalten> definiert? (Unabhängig davon, dass atomic für ein Flag besser geeignet> ist.)
Zu UB wird oft gesagt, dass alles passieren kann, z.B. auch die HD
formatieren. DAS ist natürlich Quatsch. UB bedeutet generell einfach,
dass das Modell der abstrakten Maschine zusammenbricht.
Aber: in diesem Fall des konkurrierenden Lesens (ISR / main()) kann beim
Lesen / Schreiben der Variablen jeder Zustand entstehen, den man durch
Permutation der generieren Maschineninstruktionen erhalten kann. Und das
eben scheinbar(!) zufällig, weil der Interrupt asynchron erfolgt.
Peter D. schrieb:> Einige ARM haben einen recht eleganten Weg der globalen Sperre, man kann> temporär den Level der aktuellen Task auf den höchsten Wert setzen.
Nennt sich "priority inheritance" bzw. "priority ceiling". Dies braucht
auch jedes OS, das mit fixen Prioritäten arbeiten kann, um unbegrenzte
Prioritätsinversion zu vermeiden.
Ein Anfänger schrieb:> Ich versteh das so, dass der Compiler genau das tun muss, was im Code> dasteht, d.h. mit dem gegebenen Datentyp (!) in die Speicherstelle> schreiben, bzw. an anderer Codestelle diese auslesen.
Ja, die load/stores werden so in der abtrakten Maschine ausgeführt. In
der echten Maschine können das eben nicht-atomare Anweisungsfolgen sein.
Ein reorder volatile-volatile ist nicht möglich. Andere reorder sind
möglich (z.B. normaales load, bei normalen writes ist das tatsächlich
IB). (s.a.
Beitrag "Re: c volatile -> wann bracht mans wirklich?")
> Und dann ist es> doch egal, ob das ein Interruptflagregister ist, dass sich> hardwareseitig verändern kann oder ob es sich um eine Speicherstelle im> RAM handelt.
Nein. Die Zugriffe auf normale Speicherzellen können - und sollten -
nach der as-if Regel optimiert werden.
> Und so wäre es auch wenn es sprachlich> im Hinblick auf die Atomarität nicht definiert ist,
das ist so definiert, das eben keine Atomarität gerantiert ist.
> im Hinblick auf den> Speicherzugriff doch wohl definiert und mit Kenntnis der Hardware dann> ein legitimes Konstrukt?
Nein. Der Standard sagt ganz klar, das ein konfliktbehafteter,
konkurrierender Zugriff (read-write, write-read, write-write) UB ist.
Wilhelm M. schrieb:> Zu UB wird oft gesagt, dass alles passieren kann, z.B. auch die HD> formatieren. DAS ist natürlich Quatsch.
Ich würde das nicht verharmlosen. Ich hatte vor zwei Jahren einen GCC,
der bei fehlendem return statement kein Rücksprung erzeugte - der
Programmcounter lief einfach weiter. Da kann dann wirklich alles
passieren, je nach dem was in den Daten nach der Funktion steht.
Schlimme Dinge sind extrem unwahrscheinlich, aber nicht unmöglich.
avr schrieb:> Wilhelm M. schrieb:>> Zu UB wird oft gesagt, dass alles passieren kann, z.B. auch die HD>> formatieren. DAS ist natürlich Quatsch.>> Ich würde das nicht verharmlosen.
Das DAS bezog sich doch auf das Formatieren der HD ;-)
> Ich hatte vor zwei Jahren einen GCC,> der bei fehlendem return statement kein Rücksprung erzeugte - der> Programmcounter lief einfach weiter.
Sehr schöner und gern genommener Fall von UB ... und ein guter Grund für
-Werror ;-)
db8fs schrieb:> Ich> glaub es gab mal nen Alexandrescu-Artikel drüber, wo der das -> allerdings für C++ - quasi wie const verwendet hat, um damit> multithreaded Locks zu machen.
Nein, s.u.
> Ist aber schon "hornbeinalt" der Artikel,> da gabs noch keinen scoped_lock, keinen std::mutex usw.
Nein, hat damit nichts zu tun, RAII-style Locker konnte man sich schon
immer schreiben.
Der Hintergrund von diesem (m.E. sehr missverständlichem)
https://erdani.org/publications/cuj-02-2001.php.html
Artikel ist einzig der "Missbrauch" von `volatile` als Kennzeichnung
für Datentypen, die geeignet synchronisiert sind und sich damit als
gemeinsame Datenstrukturen für die Verwendung zwischen Threads eignen.
Die Argumentation darin geht dann folgendermaßen:
1) Niemals `volatile` für primitive DT normaler Variablen (solange kein
MMIO)
verwenden (korrekt!)
2) Da C++ kein syntaktisches Konstrukt hat, um DT für die Verwendung
zwischen Threads auszuzeichnen, braucht es Dokumentation bzw. Expertise.
3) Dokumentation wird nicht gelesen und Expertise ist rar.
Daraus folgert er, dass man das Schlüsselwort `volatile` "missbrauchen"
kann,
um UDT zu kennzeichnen, die nebenläufig verwendet werden können.
Und zwar:
a) Objekte von UDT, die nebenläufig benutzt werden sollen,
werden `volatile` qualifiziert.
b) Nur die Elementfunktionen, die geeignet synchronisiert sind,
werden `volatile` qualifiziert.
c) In der Klasse werden keine Datenelemente primitiver DT `volatile`
qualifiziert.
d) Damit können nur die `volatile` Funktionen von den Threads aufgerufen
werden, die non-`volatile` eben nicht.
e) Wird dieser UDT als non-volatile verwendet (eben dann nicht zwischen
Threads,
siehe a)), können alle Elementfunktionen verwendet werden.
Das Schlimme daran: Regel a) widerspricht der Zielsetzung von
`volatile`:
denn `volatile` hat keine Garantien, die man für Nebenläufigkeit
gebrauchen könnte und darf nur für MMIO eingesetzt werden.
Es handelt sich also um den Missbrauch von `volatile` bei UDT!
Die notwendigen Memory-Barrier werden automatisch durch die notwendigen
Mutex-Operationen (lock(), unlock()) in den Elementfunktionen erledigt.
Die
Atomarität erzeugen die Mutexe.
Die Idee ist nach langer Diskussion ad-acta gelegt worden, weil sie
einfach
Mist ist. Wie man sieht, wurde sie auch nicht in die stdlib aufgenommen.
Die
UDT e.g. std::vector<> der stdlib können nicht `volatile` deklariert
werden,
weil sie keine solchen `volatile` Elementfunktionen haben.
Denn (zur Wiederholung): `volatile` ist nur für MMIO!!!
avr schrieb:> Ich hatte vor zwei Jahren einen GCC,> der bei fehlendem return statement kein Rücksprung erzeugte - der> Programmcounter lief einfach weiter.
Dann war das kein C-Compiler laut Standard. Die schließende Klammer
einer Funktion ist implizit ein return.
Das return darf nur entfallen, wenn es eine Endlosschleife ist
(unerreichbarer Code).
Vermutlich hast Du die Funktion als naked definiert und somit der
Verantwortung des Compilers entzogen.
Nein, es war eine ganz normale Funktion ohne irgendwelchen
compilerspezifischen Attribute. Es gab eine Warnung Missing return
statement und ich dachte auch erst, das kann daran nicht liegen. Im
assembly hat klar der Rücksprung gefehlt. Ich weiß leider nicht mehr
welche gcc Version es war - nachdem ich gelesen hatte, dass fehlende
return statements UB sind, habe ich von einem Bugreport abgesehen.
avr schrieb:> Nein, es war eine ganz normale Funktion ohne irgendwelchen> compilerspezifischen Attribute. Es gab eine Warnung Missing return> statement und ich dachte auch erst, das kann daran nicht liegen. Im> assembly hat klar der Rücksprung gefehlt.
Deswegen: -Werror
Und: in C darf in einer non-void Funktion das return fehlen, wenn der
(nicht vorhandene) Wert im Aufrufer nicht benutzt wird. Andernfalls ist
es UB und der Code darf Amok laufen (UB). In C++ ist es immer UB.
avr schrieb:> Es gab eine Warnung Missing return> statement und ich dachte auch erst, das kann daran nicht liegen.
Ein Compiler warnt nie grundlos. Poste mal einen compilierbaren Code mit
dieser Funktion und die vollständige Warnung.
Peter D. schrieb:> Dann war das kein C-Compiler laut Standard. Die schließende Klammer> einer Funktion ist implizit ein return.
Bei einer void Funktion. Wenn die aber einen Wert zurückgibt, gibt es
kein implizites return.
avr schrieb:> Ich hatte vor zwei Jahren einen GCC,> der bei fehlendem return statement kein Rücksprung erzeugtehttps://pvs-studio.com/en/blog/posts/cpp/0917/
Oliver
Wilhelm M. schrieb:> db8fs schrieb:>> Ich>> glaub es gab mal nen Alexandrescu-Artikel drüber, wo der das ->> allerdings für C++ - quasi wie const verwendet hat, um damit>> multithreaded Locks zu machen.>> Nein, s.u.
Doch, halt wie du selber erkannt hast, eben über den ScopedLock in
Verbindung damit.
> Nein, hat damit nichts zu tun, RAII-style Locker konnte man sich schon> immer schreiben.
Ich geb dir recht, dass volatile von der oben diskutierten Semantik dann
eben weg ist - nämlich der, nur externe Sachen zu machen. Annahme war
halt, dass volatile für class/struct relativ frei user-definierbar ist.
Heißt also, dass es da keine wirkliche Vorgabe seitens des Standards gab
dafür - demzufolge stehts ja dem Implementierer frei, das zu machen.
> Artikel ist einzig der "Missbrauch" von `volatile` als Kennzeichnung> für Datentypen, die geeignet synchronisiert sind und sich damit als> gemeinsame Datenstrukturen für die Verwendung zwischen Threads eignen.
Jo, und warum auch nicht? Wie oft hat man's schon gesehen, dass Klassen
so zugemistet sind und ohne Sinn und Verstand Multi-Threaded verwendet
werden.
Tatsächlich fehlt mir in C++ manchmal sowas, um Klassen markieren zu
können, die explizit Adapter zwischen mehreren Threadkontexten
darstellen.
> Die Idee ist nach langer Diskussion ad-acta gelegt worden, weil sie> einfach> Mist ist. Wie man sieht, wurde sie auch nicht in die stdlib aufgenommen.
Volatile ist in C++ bloß für POD relevant, für structs/classes halt
nicht. Wenn's was besseres gäbe für aktive Klassen, könnte man das auch
nehmen.
> Die> UDT e.g. std::vector<> der stdlib können nicht `volatile` deklariert> werden,> weil sie keine solchen `volatile` Elementfunktionen haben.
Richtig, wobei man ab und zu schon mal mit Lock 'Threadsafe'-gemachte
Container in der Praxis schon gesehen hatte. Solange es auch Lock-free
geht, vermisse ich das aber auch nicht.
db8fs schrieb:> Tatsächlich fehlt mir in C++ manchmal sowas, um Klassen markieren zu> können, die explizit Adapter zwischen mehreren Threadkontexten> darstellen.
Geht nicht, weil Threads Laufzeitkonstrukte sind. Was Du aber willst ist
eine statische Prüfung.
Was aber geht und gemacht, ist zuzusichern, dass unsynchronisierte
Elementfunktionen bspw. nur vom besitzenden Thread aufgerufen werden.
Wilhelm M. schrieb:> Es geht um C, nicht um C++:
Mein Beispiel war C++.
Peter D. schrieb:> Ein Compiler warnt nie grundlos.
Grundlos nie, aber oft doch ohne, dass es gleich UB wird. Ihr müsst mir
aber nicht erzählen, dass man Werror nutzen soll. Ich plädiere in der
Firma schon länger dafür, aber das durchzusetzen ist nicht unbedingt
ganz einfach.
Wilhelm M. schrieb:> avr schrieb:>> Wilhelm M. schrieb:>>> Es geht um C, nicht um C++:>>>> Mein Beispiel war C++.>> Das war nicht zu erkennen.> Jedoch ist es dann eben immer UB!
Naja, offensichtlich schon wenn man etwas Ahnung hat. Das mit UB eh
klar.
avr schrieb:> Naja, offensichtlich schon wenn man etwas Ahnung hat. Das mit UB eh> klar.
Das hier ist ein anderer aber
db8fs schrieb:> Ein Compiler warnt nie grundlos. Poste mal einen compilierbaren Code mit> dieser Funktion und die vollständige Warnung.
Zu meinem Beispiel ist alles gesagt, inklusive, dass es UB ist. Es war
auch nur, um zu zeigen, dass UB zu Amoklaufenden Code führen kann. Für
weiteres hat Oliver auch einen Link gepostet.
avr schrieb:> Grundlos nie, aber oft doch ohne, dass es gleich UB wird. Ihr müsst mir> aber nicht erzählen, dass man Werror nutzen soll.
Habe ich trotzdem gemacht ;-)
Was kommen denn da für schwachsinnige Gegenargumente?
avr schrieb:> Wilhelm M. schrieb:>> avr schrieb:>>> Keine Lust Warnungen zu entfernen.>>>> Na, das ist schwachsinnig.>> Dein Leben ist schwachsinnig
Na endlich kommen die Ausfälligkeiten ... ich dachte schon, es ist das
falsche Forum ;-)
avr schrieb:> Dein Leben ist schwachsinnig
Kann man Mal die IP Adresse von dem Troll sperren?
Wilhelm M. schrieb:> avr schrieb:>> Keine Lust Warnungen zu entfernen.>> Na, das ist schwachsinnig.
Und ja, da sind wir uns einig.
Ach, ihr kreischt euch ja noch immer gegenseitig an.
Habt ihr zwischendurch auch mal daran gedacht, wozu 'volatile'
eigentlich gut sein soll?
Nun, das Optimieren des zu erzeugenden Maschinencodes wird gerade (aber
nicht nur) beim GCC gern so betrieben, daß er gelesene Daten gern in
einem der CPU-Register zwischenspeichert, so daß er bei einer erneuten
Verwendung die eigentliche Quelle nicht nochmal lesen muß. Wenn nun nach
dem ersten Lesen sich etwas an den Daten der Quelle ändert, kriegt das
der erzeugte Maschinencode nicht mit. Um sowas zu verhindern, teilt man
ihm mittels 'volatile' mit, daß dem so sein kann und daß er folglich
Maschinencode erzeugt, der die Daten von besagter Quelle jedesmal erneut
liest.
So.
Das alles hat aber überhaupt nichts zu tun mit dem Regeln des Zugriffs
mehrerer Instanzen auf die gleichen Variablen. Sowas ist Sache des
Programmierers.
Und nun kommt mal wieder von euren diversen Palmen herunter.
Und lernt zu verstehen, wie euer GCC funktioniert ohne ihm geradezu
magische Fähigkeiten zuzuschreiben.
W.S.
Wilhelm M. schrieb:> Deswegen: -Werror
Off topic hier: schaltest Du dann nur die schlimmsten Warnungen ein,
z.b. mit -Wall? Oder hast Du verschiedene Läufe (Analyse und Build) mit
verschiedenen extra-Warnungen. Oder ein paar ignores?
W.S. schrieb:> Habt ihr zwischendurch auch mal daran gedacht, wozu 'volatile'> eigentlich gut sein soll?
Ach komm, Du hast meine Beiträge doch auch gelesen, oder?
A. S. schrieb:> Wilhelm M. schrieb:>> Deswegen: -Werror>> Off topic hier: schaltest Du dann nur die schlimmsten Warnungen ein,> z.b. mit -Wall? Oder hast Du verschiedene Läufe (Analyse und Build) mit> verschiedenen extra-Warnungen. Oder ein paar ignores?
Die policy hier ist recht stringent: ggf. viele weitere Warnungen
neben -Werror -Wall -Wextra -Wstrict-aliasing=1 -Wconversion -Wswitch
-Wswitch-enum -Wshift-count-overflow (-Waddr-space-convert) und dann
gezielt Plattform/Compiler-spezifische ignores, die mit einem
verifying-example abgesichert sein müssen.
Interessant, da werden wegen maximal schwachen Konstruktionen, zB
volatilen Pointergeschichten, C/Pascal Kriege ausgegraben...
Bei Volatile geht's um Interrupts. Interrupts sollten maximal kurz sein.
Also eher nicht Poiner auf Struct. Dort kommt man eigentlich nur hin,
wenn man ein RTOS selbst schreibt, und den Stack und Heap wechseln muss.
Mein Pascal Compiler kennt zB kein volatile, er ist zu faul, in einem
Interrupt verwendete Variablen zu markieren, er optimiert solche
Konstrukte auch nicht. Etwas mitdenken sollte man auch noch.
Purzel H. schrieb:> Bei Volatile geht's um Interrupts.
Immer noch nein: s.a.
Beitrag "Re: c volatile -> wann bracht mans wirklich?"> Interrupts sollten maximal kurz sein.
Hat mit volatile nichts zu tun.
> Also eher nicht Poiner auf Struct.
Bezugslos.
> Dort kommt man eigentlich nur hin,> wenn man ein RTOS selbst schreibt, und den Stack und Heap wechseln muss.
Nö.