Forum: Compiler & IDEs volatile, die 100.


von Masl (Gast)


Lesenswert?

Hallo,

Variablen die in der main gelesen und in einem Interrupt geschrieben 
werden sollte man ja mit volatile kennzeichnen.
In einer ISR muss ja der Compiler die Variable eh immer aus dem RAM 
holen und zurückschreiben, da er ja den Kontext des Aufrufs nicht kennt.
In der main könnte er folgern dass die Variable nicht beschrieben wird 
(d.h. sich nicht geändert hat) und deswegen z.B. Vergleiche 
wegoptimieren. Grund: er weiß ja nicht dass ein IR zuschlagen kann.

Wenn ich nun aber z.B. ein UART-Modul habe dass einen internen 
Ringpuffer verwendet und per Header-Datei seine Schnittstellen zur main 
bekanntmacht.
Der Compiler übersetzt ja die UART.c erstmal für sich. Er sieht aber 
dass Funktionen die nicht static sind theoretisch von einem ihm zu 
diesem Zeitpunkt unbekannten Modul aufgerufen werden können. D.h. er 
kann eigentlich nichts in Registern halten, richtig?

Muss ich nun diesen Puffer auch volatile machen?

Viele Grüße,
Masl

von HutHut (Gast)


Lesenswert?

gibts im UART Modul eine ISR die auf den Puffer zugreift?
-> Ja: volatile
-> Nein: Kommt auf die genaue Anwendung drauf an.

http://en.wikipedia.org/wiki/Critical_section

von Masl (Gast)


Lesenswert?

Ja, die gibts natürlich, die schreibt ja das empfangene Byte in den 
Puffer.

Daneben existiert eine Funktion die ein Byte daraus lesen kann.
Mein Gedankengang dahinter war aber, dass der Compiler in dieser 
Funktion eigentlich immer den Puffer neu aus dem RAM laden muss, weil 
die Funktion ja theoretisch von einem anderen Modul aufgerufen werden 
kann, solang sie nicht static ist.

Oder woher soll der Compiler folgern dass sich die Register nicht 
geändert haben? Er kennt ja nicht den kompletten Kontrollfluß über 
mehrere c-Dateien.

von Masl (Gast)


Lesenswert?

Nochwas: was hat volatile mit dem von dir verlinkten Artikel zu tun?
Eine kritische Situation kann auch bei volatile noch entstehen, nämlich 
wenn ich z.B. auf einem 8-Bitter einen 32-Bit Zugriff durchführe. Dieser 
kann natürlich unterbrochen werden, volatile hin- oder her.
Um das zu vermeiden braucht man Mutexe oder Interruptsperren, aber das 
ist imho eine ganz andere Geschichte.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Masl schrieb:

> Oder woher soll der Compiler folgern dass sich die Register nicht
> geändert haben? Er kennt ja nicht den kompletten Kontrollfluß über
> mehrere c-Dateien.

Kommt auf die aktivierten Optimierungen an.  Mit -flto hat er zum 
Beispiel globale Kontrollfluß-Info — vorausgesetzt, LTO wird richtig 
angewendt :-)

von Bronco (Gast)


Lesenswert?

Wenn ich Dich richtig verstehe, meinst Du so etwas wie das hier 
(vereinfacht):
1
static uint8_t puffer[100];
2
static uint8_t index = 0;
3
uint8_t GetNextByte(void)
4
{
5
  uint8_t x = puffer[index];
6
  index++; 
7
  if (index >= 100) index = 0;
8
  return( x );
9
}

Und jetzt überlegst Du, ob "puffer" volatile sein muß oder nicht, wo 
doch GetNextByte() einfach ganz stur Speicheradressen ausliest und es da 
nix zu optimieren gibt - richtig?

Wow, gute Frage!
Wahrscheinlich hilft es letztendlich nur, den Assembler-Code anschauen, 
heutzutage können die Compiler ja wirklich unfaßbar gut optimieren.

von Fabian O. (xfr)


Lesenswert?

Am sichersten dürfte es sein, wenn man sich das Programm konzeptionell 
so vorstellt, dass alles in einer c-Datei steht und alle Funktionen 
geinlint sind, es also nur noch die main-Funktion gibt. Alles, was sich 
außerhalb dieses Kontrollflusses ändern kann, würde ich volatile setzen.

von Karl H. (kbuchegg)


Lesenswert?

Bronco schrieb:

> Wow, gute Frage!

Nicht wirklich .....


Bei volatile geht es tatsächlich nur um den Fall, wenn es einen 2.ten 
Programpfad gibt bzw. eine andere Möglichkeit, wie sich der Inhalt einer 
Variablen ändern kann, ohne dass es für den Compiler ersichtlich ist.

Um mehr gehts da nicht.

Und das ist, wenn man nur diese Funktion betrachtet, nun mal nicht der 
Fall.

Da scheint ein Missverständnis bei Masl vorzuliegen.
Der Compiler kann sowieso nicht davon ausgehen, dass er zb index 
einmalig in ein Register lädt und dann immer nur dieses Register 
weiterverwendet. Eine Funktion ist aus Sprach-Sicht eine in sich 
abgeschlossene Einheit. Die Funktion wird betreten und der Compiler darf 
erst mal von nichts ausgehen sondern muss die Arbeitsumgebung 
herstellen, die die Funktion braucht um arbeiten zu können. Die Funktion 
wird irgendwann mal fertig und dann wird diese Arbeitsumgebung wieder 
aufgelöst.

Das ist die Ausgangssituation auf der Optimizer aufbauen und unter 
Umständen gewisse Dinge rauswerfen können. Aber immer gilt dabei 'As 
if'. d.h. der Optimizer darf keine Veränderungen machen, die das 
konzeptionelle Bild, so wie es sich aus der Sprachdefinition ergibt, 
über den Haufen wirft. Das heißt, der Optimizer darf eine Optimierung 
nur dann machen, wenn sichergestellt ist (er das nachweisen kann), dass 
er an der Funktionalität nichts verändert.

volatile ist so gesehen eine Ausnahme davon, die man akzeptieren muss, 
weil ein Verzicht darauf auf der einen Seite zu schwerwiegende 
Einschränkungen im Optimizing-Prozess führen würde und auf der anderen 
Seite das Einsatzgebiet von C zu stark einschränken würde.


> Der Compiler übersetzt ja die UART.c erstmal für sich. Er sieht aber
> dass Funktionen die nicht static sind theoretisch von einem ihm zu
> diesem Zeitpunkt unbekannten Modul aufgerufen werden können. D.h.
> er kann eigentlich nichts in Registern halten, richtig?

Grundsätzlich gibt es keinen Passus im Sprachstandard, der dieses 
verbieten würde, WENN es möglich wäre. Aber aus praktischen Gründen ist 
das auf einer Architektur wie dem AVR nicht durchführbar. So ja. Er kann 
sich den Wert nicht in einem Register bis zum nächsten Aufruf vorhalten.
Aber das ist nicht dein Problem. Das ist ein Problem, mit dem sich die 
Compilerbauer rumschlagen müssen, ob und wenn ja wie sie ein derartiges 
Feature implementieren könnten.

von Bronco (Gast)


Lesenswert?

Karl Heinz Buchegger schrieb:
> Bei volatile geht es tatsächlich nur um den Fall, wenn es einen 2.ten
> Programpfad gibt bzw. eine andere Möglichkeit, wie sich der Inhalt einer
> Variablen ändern kann, ohne dass es für den Compiler ersichtlich ist.

Bezieht sich das explizit auf "eine Variable"?

Wenn ich es richtig verstehe, geht es um folgendes:
Wir haben ein Array, also einen Speicherbereich, da schreibt die ISR 
hinein und eine aus Main aufgerufene Funktion liest aus. Der 
Lese-Zugriff erfolgt über einen Pointer (bzw. Array mit Index).
Die Frage ist: würde der Compiler ein solchen Speicher-Auslese-Zugriff 
mit veränderlicher Adresse wegoptimieren, weil er denkt, der Speicher 
ist eh immer konstant?
Wohl kaum. Also braucht man auch kein volatile, oder?

von Peter D. (peda)


Lesenswert?

Fabian O. schrieb:
> Am sichersten dürfte es sein, wenn man sich das Programm konzeptionell
> so vorstellt, dass alles in einer c-Datei steht und alle Funktionen
> geinlint sind, es also nur noch die main-Funktion gibt.

Das ist der sichere Ansatz.

In diesem Fall, wenn der Puffer 100 Byte ist, ist man theoretisch 
sicher.
Aber wenn der Puffer z.B. nur 4 Byte ist, könnte sich der Compiler 
sagen, ich halte ihn in 4 Registern und dann kracht es.
Korrekt müßte er also volatile sein.

Man kann aber auch nur den Mainzugriff casten. Das hat den Vorteil, daß 
man beim Lesen immer darauf aufmerksam wird, daß hier ein Interrupt im 
Spiel ist.
1
// force access of interrupt variables
2
#define IVAR(x)         (*(volatile typeof(x)*)&(x))
3
4
static uint8_t puffer[100];
5
static uint8_t index = 0;
6
7
uint8_t GetNextByte(void)
8
{
9
  uint8_t x = IVAR(puffer[index]);
10
  index++; 
11
  if (index >= 100) index = 0;
12
  return( x );
13
}


Peter

von Fabian O. (xfr)


Lesenswert?

Peter Dannegger schrieb:
> In diesem Fall, wenn der Puffer 100 Byte ist, ist man theoretisch
> sicher.
> Aber wenn der Puffer z.B. nur 4 Byte ist, könnte sich der Compiler
> sagen, ich halte ihn in 4 Registern und dann kracht es.
> Korrekt müßte er also volatile sein.

Er könnte die Zugriffe auf den Puffer auch komplett wegoptimieren. Mal 
ein vollständiges Beispiel (der Code ist sinnlos, aber darum geht es ja 
nicht):
1
// main.c
2
3
static uint8_t puffer[100] = {0};
4
static uint8_t index = 0;
5
6
static uint8_t GetNextByte(void)
7
{
8
  uint8_t x = puffer[index];
9
  index++; 
10
  if (index >= 100) index = 0;
11
  return( x );
12
}
13
14
static void SetNextByte(uint8_t x)
15
{
16
  puffer[index] = x;
17
  index++;
18
}
19
20
void main(void)
21
{
22
  while (1) {
23
    uint8_t x = GetNextByte();
24
    putchar(x);
25
  }
26
}
27
28
ISR()
29
{
30
  setNextByte(HARDWARE_REGISTER);
31
}

Die Funktion GetNextByte() kann nicht von außerhalb aufgerufen werden 
(da static) und sie wird nur einmal von main() aus aufgerufen, also wird 
man sie inlinen:
1
void main(void)
2
{
3
  while (1) {
4
    uint8_t x = puffer[index];
5
    index++;
6
    if (index >= 100) index = 0;
7
    putchar(x);
8
  }
9
}

Wann die ISR zuschlägt, weiß der Compiler nicht, also sieht er nur, dass 
laufend puffer[0] bis puffer[99] ausgegeben wird. Das Array puffer wurde 
mit 0 initialisiert und (ohne die ISR) nie beschrieben. Also kann er das 
ganze optimieren zu:
1
void main(void)
2
{
3
  while (1) {
4
    putchar(0);
5
  }
6
}

Die ursprüngliche Frage war aber, wie es ist, wenn GetNextByte() und 
main() in verschiedenen C-Dateien stehen. Dann weiß der Compiler beim 
Compilieren von GetNextByte() nicht, ob und wann SetNextByte() 
aufgerufen wird. Also wird er den Array-Zugriff nicht wegoptimieren. Das 
gilt aber nur, wenn nach dem Compilieren der c-Dateien keine 
modulübergreifende Optimierung des Gesamtprogramms mehr stattfindet. Und 
darauf würde ich mich nicht verlassen, denn das hängt vom Compiler bzw. 
dessen Einstellung ab (siehe Post von Johann L.).

von Oliver S. (oliverso)


Lesenswert?

Fabian O. schrieb:
> Die ursprüngliche Frage war aber, wie es ist, wenn GetNextByte() und
> main() in verschiedenen C-Dateien stehen. Dann weiß der Compiler beim
> Compilieren von GetNextByte() nicht, ob und wann SetNextByte()
> aufgerufen wird.

Das weiß er auch dann nicht, wenn die beiden Funktionen in einer Datei 
stehen. Solange ihm nicht explizit per option oder per static gesagt 
wird, daß das die einzige Sourcedatei ist, geht der immer davon aus, daß 
es noch weitere Dateien geben kann.

Oliver

von Masl (Gast)


Lesenswert?

Dieses Beispiel trifft meine ursprüngliche Frage sehr gut :-)

Also kann man abschließend sagen dass es von einer Vielzahl an Faktoren 
im Programmaufbau sowie Compileroptionen abhängt. Deswegen ist das 
Deklarieren, oder zumindest das Casten durch volatile anzuraten.


Diese ominöse modul-übergreifend-optimieren-Option bringt mich aber nun 
zu einer neuen Frage:
Beispiel: Ein Interrupt löst aus. Über eine Callback-Funktion melde ich 
einer höheren Schicht (= anderes Modul) das ein bestimmtes Event 
eingetreten ist.
Im Prinzip nur ein Funktionsaufruf und das Setzen eines Flags.

Bisher hab ich mich immer daran gestört dass bei so einem 
Funktionsaufruf innerhalb einer ISR (die Funktion setzt nur ein Flag, 
mehr nicht!) der Kontext komplett gesichert werden muss.
D.h. die ISR die eigentlich nicht viel macht ist hauptsächlich damit 
beschäftigt, die Register erst alle auf den Stack zu pushen und später 
wieder zu popen. (Jeweils 2 Takte => Flash und Laufzeit).

Würde dieses Verhalten durch diese Compiler-Option verhindert werden?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Masl schrieb:

> Diese ominöse modul-übergreifend-optimieren-Option bringt mich aber nun
> zu einer neuen Frage:
> Beispiel: Ein Interrupt löst aus. Über eine Callback-Funktion melde ich
> einer höheren Schicht (= anderes Modul) das ein bestimmtes Event
> eingetreten ist.
> Im Prinzip nur ein Funktionsaufruf und das Setzen eines Flags.
>
> Bisher hab ich mich immer daran gestört dass bei so einem
> Funktionsaufruf innerhalb einer ISR (die Funktion setzt nur ein Flag,
> mehr nicht!) der Kontext komplett gesichert werden muss.
> D.h. die ISR die eigentlich nicht viel macht ist hauptsächlich damit
> beschäftigt, die Register erst alle auf den Stack zu pushen und später
> wieder zu popen. (Jeweils 2 Takte => Flash und Laufzeit).
>
> Würde dieses Verhalten durch diese Compiler-Option verhindert werden?

Ja.

von Bronco (Gast)


Lesenswert?

Karl Heinz Buchegger schrieb:
>> Wow, gute Frage!
> Nicht wirklich .....

Anscheindend doch, denn

Fabian O. schrieb:
> Wann die ISR zuschlägt, weiß der Compiler nicht, also sieht er nur, dass
> laufend puffer[0] bis puffer[99] ausgegeben wird. Das Array puffer wurde
> mit 0 initialisiert und (ohne die ISR) nie beschrieben. Also kann er das
> ganze optimieren zu:
1
void main(void)
2
{
3
  while (1) {
4
    putchar(0);
5
  }
6
}

Wenn es stimmt, daß Compiler so optimieren darf, muß man den puffer doch 
mit volatile kennzeichnen, oder PeDa's IVAR Ansatz gehen.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.