c volatile -> wann bracht mans wirklich?

Gast #7298005
Lesenswert?

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
typedef struct tFoo{
2
  volatile uint8_t * volatile SomeArray;
3
  //...
4
} tFoo;
5

6
volatile tFoo * volatile foo;

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
void somefct(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!
Beitrag #7298088 wurde von einem Moderator gelöscht.
#7298106
Lesenswert?

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
 __ASM volatile ("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.
Gast #7298345
Lesenswert?

Ε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.
#7298392
Lesenswert?

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.
Gast #7298509
Lesenswert?

Ε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.
#7298675
Lesenswert?

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
asm volatile ("" : : : "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.
#7298698
Lesenswert?

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:
1
#define IVAR(x)         (*(volatile typeof(x)*)&(x))
Gast #7298975
Lesenswert?

Ε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
> #define IVAR(x)         (*(volatile typeof(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
typedef struct tFoo tFoo;
3
tFoo * foo; //Pointer auf ein Array des Structs
4

5
//In einer Funktion
6
tFoo * localFoo;
7

8
localFoo= (volatile tFoo*)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
1
localFoo= IVAR(foo)+3;
Gast #7299044
Lesenswert?

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.
Gast #7299506
Lesenswert?

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.
Gast #7300207
Lesenswert?

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?
Gast #7300236
Lesenswert?

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.
Gast #7300286
Lesenswert?

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.
Gast #7300392
Lesenswert?

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:)
Gast #7300410
Lesenswert?

Ein Anfänger schrieb:
> Wie meinst du das?

Der Standard-Anwendungsfall ist
1
interrupt Ix(void)
2
{
3
   ...
4
   flg = 0;
5
   ...
6
}
7

8

9
int main(void)
10
{
11
  ....
12
  flg=1;
13
  while(flg);
14
  ...
15
}
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 ;-)
#7300558
Lesenswert?

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.
Gast #7300862
Lesenswert?

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!
Gast #7300886
Lesenswert?

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.
Gast #7301185
Lesenswert?

A. S. schrieb:
> ändert man die while-Anweisung zu
>
1
>   while(flg) {myWait();}
2
>
> 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.
Gast #7301947
Lesenswert?

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'.

#7301953
Lesenswert?

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

Gast #7302089
Lesenswert?

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:

1
struct Foo
2
{
3
   int TollerIntWert;
4
   struct { int c : 16; int d : 16} TollesBitfield;
5
};
6

7
volatile struct Foo* blub = (volatile struct Foo*)(0xADD);

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:

1
struct Foo kopie;
2
(void) memcpy(&kopie, (void*)blub, sizeof(struct Foo));

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.

#7302097
Lesenswert?

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

Gast #7302153
Lesenswert?

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.

Gast #7302247
Lesenswert?

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.

Gast #7302272
Lesenswert?

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.

Moderator #7302313
Lesenswert?

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.

Gast #7302345
Lesenswert?

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.

Moderator #7302387
Lesenswert?

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.

Gast #7302602
Lesenswert?

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.

Gast #7302667
Lesenswert?

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
int main(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.

#7302676
Lesenswert?

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

Gast #7302934
Lesenswert?

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:

1
void meinTollerLogger(char* const text)
2
{
3
  printf(text);
4
}
5

6
void (*fnLogger)(char* const text) = &meinTollerLogger;
7

8
void main()
9
{
10
  fnLogger("foobar");
11
}

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.

#7303048
Lesenswert?

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.

Gast #7303073
Lesenswert?

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 ...)

Gast #7303798
Lesenswert?

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.

#7303928
Lesenswert?

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?")

#7303993
Lesenswert?

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.

#7304036
Lesenswert?

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.

#7304099
Lesenswert?

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.

#7304107
Lesenswert?

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
> #define IVAR(x)         (*(volatile typeof(x)*)&(x))
2
>

Ich wäre auch bereit, ein gleichwertiges Memory-Barrier Macro zu benutzen.

s.a. _MemoryBarrier()

#7304120
Lesenswert?

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

Gast #7304130
Lesenswert?

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.

Gast #7304137
Lesenswert?

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.

#7304155
Lesenswert?

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

Gast #7304165
Lesenswert?

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.

#7304170
Lesenswert?

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.

Gast #7304173
Lesenswert?

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.

#7304177
Lesenswert?

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 ...

Gast #7304208
Lesenswert?

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...

#7304210
Lesenswert?

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.

Gast #7304212
Lesenswert?

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.

#7304218
Lesenswert?

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.

Gast #7304222
Lesenswert?

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.

#7304228
Lesenswert?

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

#7304234
Lesenswert?

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.

#7304238
Lesenswert?

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.

Gast #7304253
Lesenswert?

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.)

#7304273
Lesenswert?

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).

Gast #7304299
Lesenswert?

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?

#7304304
Lesenswert?

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.

Gast #7304320
Lesenswert?

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?

Gast #7304331
Lesenswert?

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?

#7304345
Lesenswert?

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).

Gast #7304379
Lesenswert?

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.

Gast #7304586
Lesenswert?

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.

Gast #7304632
Lesenswert?

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.)

Gast #7304654
Lesenswert?

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.

#7304701
Lesenswert?

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.

#7305001
Lesenswert?

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.

#7305026
Lesenswert?

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.

Gast #7305103
Lesenswert?

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.

#7305104
Lesenswert?

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 ;-)

#7305130
Lesenswert?

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!!!

#7305278
Lesenswert?

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.

Gast #7305381
Lesenswert?

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.

#7305388
Lesenswert?

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.

#7305510
Lesenswert?

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 erzeugte

https://pvs-studio.com/en/blog/posts/cpp/0917/

Oliver

#7305516
Lesenswert?

Oliver S. schrieb:

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.

Es geht um C, nicht um C++:

Beitrag "Re: c volatile -> wann bracht mans wirklich?"

avr schrieb:

Ich hatte vor zwei Jahren einen GCC, der bei fehlendem return statement kein Rücksprung erzeugte

https://pvs-studio.com/en/blog/posts/cpp/0917/

Beitrag "Re: c volatile -> wann bracht mans wirklich?"

Gast #7305517
Lesenswert?

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.

#7305544
Lesenswert?

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.

Gast #7305683
Lesenswert?

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.

Gast #7305895
Lesenswert?

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.

#7305955
Lesenswert?

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.

#7307210
Lesenswert?

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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren