Forum: Compiler & IDEs volatile Variablen lokal zwischenspeichern?


von Robert F. (fastred)


Lesenswert?

Guten Tag!

Ich schaue mir gerade an, wie die millis()-Funktion bei Arduino 
implementiert ist. Diese liefert die Anzahl der ms seit dem Start des 
uC's zurück. In
1
/arduino-0017hardware/cores/arduino/wiring.c

findet sich dessen Implementierung:
1
volatile unsigned long timer0_overflow_count = 0;
2
volatile unsigned long timer0_millis = 0;
3
static unsigned char timer0_fract = 0;
4
5
SIGNAL(TIMER0_OVF_vect)
6
{
7
  // copy these to local variables so they can be stored in registers
8
  // (volatile variables must be read from memory on every access)
9
  unsigned long m = timer0_millis;
10
  unsigned char f = timer0_fract;
11
12
  m += MILLIS_INC;
13
  f += FRACT_INC;
14
  if (f >= FRACT_MAX) {
15
    f -= FRACT_MAX;
16
    m += 1;
17
  }
18
19
  timer0_fract = f;
20
  timer0_millis = m;
21
  timer0_overflow_count++;
22
}
23
24
25
unsigned long millis()
26
{
27
  unsigned long m;
28
  uint8_t oldSREG = SREG;
29
30
  // disable interrupts while we read timer0_millis or we might get an
31
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
32
  cli();
33
  m = timer0_millis;
34
  SREG = oldSREG;
35
36
  return m;
37
}

Prinzipiell wenig überraschendes, Timer0 läuft beim Arduino durch und 
setzt u.a. den Wert für  m bzw.  timer0_millis hoch. Das würde meine 
Recherche ja befriedigen, allerdings wundere ich mich warum die Jungs 
die volatiles nochmal explizit zwischenspeichern:
1
  // copy these to local variables so they can be stored in registers
2
  // (volatile variables must be read from memory on every access)
3
  unsigned long m = timer0_millis;
4
  unsigned char f = timer0_fract;

Meinem Wissen nach, weist volatile den Compiler ja an, die Variable 
jedes mal ausm SRAM zu holen.

In meinem C-Buch ( Goto Reihe, Addison-Wesley) steht nur :
"[...] volatile ist ein Hinweis an den Compiler, bei jedem Zugriff auf 
die Variable die zugehörige Speicherstelle auszulesen, selbst wenn die 
Variable bereits in einem schnellen internen Register steht."

Ist meine Interpretation falsch oder sind die Jungs von Arduino/Wiring 
nur (über)vorsichtig?

Vielen Dank!

von Peter (Gast)


Lesenswert?

Robert F. schrieb:
> Meinem Wissen nach, weist volatile den Compiler ja an, die Variable
> jedes mal ausm SRAM zu holen.

genau das ist ja das Problem, das ist langsam. Wenn du sie ein eine 
lokale Variabel speicherst dann kann der Compieler sie in einem Register 
speichern. Damit geht der Zugriff schneller.

von jl (Gast)


Lesenswert?

ganz klar um im Interrupt Rechenzeit zu sparen. Nach der Zuweisung sind 
sie im Register und muss nicht immer mit Adressbefehlen erst herangeholt 
werden.

Das volatile ist ja nur wegen dem Anwendungsprogramm drin damit das 
nicht mit alten daten arbeitet.

von Robert F. (fastred)


Lesenswert?

Also wenn man sie mehrfach in einer Methode .. ähh Funktion verwendet, 
sollte man sie Zwischenspeichern, klar.
Aber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein 
Performanzgewinn...

von Peter (Gast)


Lesenswert?

Robert F. schrieb:
> ber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein
> Performanzgewinn...

ich zähle 3 bzw 4 zugriffe.

von spacedog (Gast)


Lesenswert?

Robert F. schrieb:
> Also kein
> Performanzgewinn...

Aber ein "Atomizitätsgewinn". Es könnte ja sein, dass wenn du millis() 
ausliest, gerade mittendrin der Timer-Interrupt kommt und dir den Wert 
verändert, den du schon zur Hälfte ausgelesen hast.

von Karl H. (kbuchegg)


Lesenswert?

Robert F. schrieb:
> Also wenn man sie mehrfach in einer Methode .. ähh Funktion verwendet,
> sollte man sie Zwischenspeichern, klar.

Das kann man so pauschal auch wieder nicht sagen.
Das bringt nur hier etwas, weil es sich um eine ISR handelt. D.h. wir 
wissen an dieser Stelle, dass sich die Variablen sowieso nicht verändern 
können! Wir sind ja gerade in der Funktion, die die Veränderung 
durchführt.

> Aber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein
> Performanzgewinn...

Schau nochmal genau

Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht 
signifikant. Aber es geht ja ums Prinzip.

von Daniel V. (danvet)


Lesenswert?

Deine Überlegungen sind nicht ganz richtig.

statt:
1
m += MILLIS_INC;
2
  f += FRACT_INC;
3
  if (f >= FRACT_MAX) {
4
    f -= FRACT_MAX;
5
    m += 1;
6
  }

könnten die ja auch
1
timer0_millis += MILLIS_INC;
2
  timer0_fract += FRACT_INC;
3
  if (timer0_fract >= FRACT_MAX) {
4
    timer0_fract -= FRACT_MAX;
5
    timer0_millis += 1;
6
  }

schreiben, richtig? NEIN!

Falls du Assembler kannst, dann schau dir mal die unterschiedlichen 
Assemblerlistings an.
Ansonsten:
volatile besagt, dass die Variable IMMER aus dem SRAM gelesen werden 
muss. Demzufolge muss sie auch augenblicklich wieder dothin zurück 
geschrieben werden. D.h. im obigen Beispiel würden die Variablen immer 
vom SRAM in ein Register gelesen werden und das Ergebnis würde sofort 
wieder ins SRAM geschrieben, auch wenn die Variable später nochmal 
gebraucht wird (dann wird sie halt wieder ausdem SRAM gelesen). Das 
erzeugt einen Overhead, der (zumindest in der ISR) nicht benötigt wird. 
Das Zwischenspeichern in die lokale Variable (=laden in ein Register) 
hält die volatile Variable im Register. Alle weiteren Berechnungen 
finden mit diesem Register statt.

Es wird bei beiden Codes das gleiche rauskommen, aber mit 
Registerzwischenspeicher gehts deutliche schneller.

EDIT: uuups war zu langsam

von Robert F. (fastred)


Lesenswert?

Peter schrieb:
> Robert F. schrieb:
>> ber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein
>> Performanzgewinn...
>
> ich zähle 3 bzw 4 zugriffe.

das "1x" bezog sich auf millis() ...
in der ISR zähle ich ( wenn man sich die Zwischenspeicherung weg denkt) 
einen Zugriff beim incrementieren, einen bedingten, also 1 oder 2.

in der Summe komme ich dann auf 2-3 ... habe ich mich verzählt/ etwas 
übersehen?

spacedog schrieb:
> Aber ein "Atomizitätsgewinn". Es könnte ja sein, dass wenn du millis()
> ausliest, gerade mittendrin der Timer-Interrupt kommt und dir den Wert
> verändert, den du schon zur Hälfte ausgelesen hast.

naja dafür ist ja das SREG-save&resore + cli() Konstrukt da (hier im 
Bsp.)

1
  // (volatile variables must be read from memory on every access)
^ Dieser Kommentar ist dann aber stark irreführend...

Karl heinz Buchegger schrieb:
>
> Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht
> signifikant. Aber es geht ja ums Prinzip.

... also - an dieser Stelle / in diesem Bsp. - Übervorsicht?

Daniel V. schrieb:
> EDIT: uuups war zu langsam
^ hehe im Forum braucht man auch volatile, die Threads werden während 
der Ansicht extern verändert ;)

von Robert F. (fastred)


Lesenswert?

Daniel V. schrieb:
> Deine Überlegungen sind nicht ganz richtig.

^ kannst du ggf. dazu sagen, welche meiner Überlegungen nicht ganz 
richtig ist? Thx!

von Peter (Gast)


Lesenswert?

Robert F. schrieb:
> arl heinz Buchegger schrieb:
>>
>> Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht
>> signifikant. Aber es geht ja ums Prinzip.
>
> ... also - an dieser Stelle / in diesem Bsp. - Übervorsicht?

teste doch mal beides und schau die den asm code an, aber ich würde fast 
sagen das diese Version doppelt so schnell ist. Auch wenn es in der 
summe nur nur im µs geht.

von Robert F. (fastred)


Lesenswert?

Peter schrieb:
> teste doch mal beides und schau die den asm code an, aber ich würde fast
> sagen das diese Version doppelt so schnell ist. Auch wenn es in der
> summe nur nur im µs geht.

Die o.g. Implementierung (Zwischenspeichern) *zusammen mit dem 
Kommentar*
1
  // (volatile variables must be read from memory on every access)
hat mich stutzig gemacht.

Daher wollte ich nachfragen, ob ich einen Denkfehler bzgl. volatile habe 
("Meinem Wissen nach, weist volatile den Compiler ja an, die Variable 
jedes mal ausm SRAM zu holen.")


Also ich wollte jetzt keine Performanzdiskursion oder über ein 
Memory/Speed Trade Off Diskutieren...

von Karl H. (kbuchegg)


Lesenswert?

Peter schrieb:
> Robert F. schrieb:
>> arl heinz Buchegger schrieb:
>>>
>>> Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht
>>> signifikant. Aber es geht ja ums Prinzip.
>>
>> ... also - an dieser Stelle / in diesem Bsp. - Übervorsicht?
>
> teste doch mal beides und schau die den asm code an, aber ich würde fast
> sagen das diese Version doppelt so schnell ist.

Nicht ganz.

timer0_fract ist sowieso schon mal nicht volatile und von daher ist ein 
Laden in lokale Variablen Luxus (den der Compiler höchst wahrscheinlich 
sowieso auf eigene Faust breeinigt)

Und timer0_millis wird nur dann ein zweites mal angerührt, wenn die 
Fraction überläuft.

Nun kennen wir natürlich nicht die exakten Werte für die ISR-Frequenz, 
FRACT_MAX und FRACT_INT, aber doppelt so schnell ist die optimierte 
Variante sicherlich nicht.

von Peter (Gast)


Lesenswert?

Karl heinz Buchegger schrieb:
> Nun kennen wir natürlich nicht die exakten Werte für die ISR-Frequenz,
> FRACT_MAX und FRACT_INT, aber doppelt so schnell ist sie sicherlich
> nicht.

stimmt, ich hatte die lese und schreibaktionen der volatile variabel 
unterschlagen, sie müssen ja mindests einmal gemacht werden.

Da sehen ich übringens als ein grossen Nachteil von volatil an. Gibt es 
nicht eine Möglichkeit eine Variable normal zu deklarieren und dann an 
der stelle wo es entscheident ist dem Compiler mitzuteilen das er bitte 
den aktuellen stand auslesen soll.

while ( read_volatil( var ) == 1 )
{
   mach was..
}

von Daniel V. (danvet)


Lesenswert?

Robert F. schrieb:
> Peter schrieb:
>> teste doch mal beides und schau die den asm code an, aber ich würde fast
>> sagen das diese Version doppelt so schnell ist. Auch wenn es in der
>> summe nur nur im µs geht.
>
> Die o.g. Implementierung (Zwischenspeichern) *zusammen mit dem
> Kommentar*
>
1
>   // (volatile variables must be read from memory on every access)
2
>
> hat mich stutzig gemacht.
>
> Daher wollte ich nachfragen, ob ich einen Denkfehler bzgl. volatile habe
> ("Meinem Wissen nach, weist volatile den Compiler ja an, die Variable
> jedes mal ausm SRAM zu holen.")
>
>

Das meinte ich mit "Deine Überlegungen sind nicht ganz richtig.". 
Vielleicht wäre "...nicht ganz vollständig." besser?
In der ISR möchte man natürlich vermeiden, dass die volatile Variable 
ständig vom SRAM gelesen werden muss, weil die  ISR nicht unterbrochen 
wird.
Deswegen der Hinweis seitens den Typen.

von Robert F. (fastred)


Lesenswert?

Daniel V. schrieb:
[...]
> Das meinte ich mit "Deine Überlegungen sind nicht ganz richtig.".
> Vielleicht wäre "...nicht ganz vollständig." besser?
> In der ISR möchte man natürlich vermeiden, dass die volatile Variable
> ständig vom SRAM gelesen werden muss, weil die  ISR nicht unterbrochen
> wird.
> Deswegen der Hinweis seitens den Typen.

Ah ok, vielen Dank.


Beim scharfen Hinsehen auf millis() viel mir auf, dass man um eine 
Zwischenspeicherung nicht herrumkommt:

* timer0_millis ist long -> beim Zugriff kann einem ein ISR dazwischen 
pfuschen
* => SREG save&restore nötig
* return timer0_millis geht hier nicht, da sonst SREG = oldSREG; fehlen 
würde

-> also wird es in millis() nicht aus Übervorsicht, oder aus Performanz 
sondern, wie spacedog bereits bemerkte wgn der atomität bzw. weil es 
nicht anders geht ;)

von Karl H. (kbuchegg)


Lesenswert?

Peter schrieb:

> Da sehen ich übringens als ein grossen Nachteil von volatil an. Gibt es
> nicht eine Möglichkeit eine Variable normal zu deklarieren und dann an
> der stelle wo es entscheident ist dem Compiler mitzuteilen das er bitte
> den aktuellen stand auslesen soll.
>
> while ( read_volatil( var ) == 1 )
> {
>    mach was..
> }

Hmm. Interessant Idee bzw. Fragestellung

1
   while( *((volatile uint8_t*)&var) == 1 )
müsste das eigentlich machen. Also einfach das volatile dazucasten :-)
Der Nachteil ist natürlich, dass hier der eigentliche Datentyp von var 
vorkommt und man sich so Probleme bei einer Datentypänderung einhandelt. 
Aber das Problem hat man im Grunde mit jedem Cast der etwas mit Pointern 
zu tun hat.

Ohne Gewähr:
1
   while( *((volatile typeof(var)*)&var) == 1 )
und dann den ganzen Klimbim in einem Makro verstecken.
1
#define ACCESS_VOLATILE(x)   *((volatile typeof(x)*)&x
2
3
4
    while( ACCESS_VOLATILE(var) == 1 )
5
      ACCESS_VOLATILE(var) = 2;

das sieht gar nicht mal so dumm aus.

von Daniel V. (danvet)


Lesenswert?

Robert F. schrieb:
> Daniel V. schrieb:
> [...]
>> Das meinte ich mit "Deine Überlegungen sind nicht ganz richtig.".
>> Vielleicht wäre "...nicht ganz vollständig." besser?
>> In der ISR möchte man natürlich vermeiden, dass die volatile Variable
>> ständig vom SRAM gelesen werden muss, weil die  ISR nicht unterbrochen
>> wird.
>> Deswegen der Hinweis seitens den Typen.
>
> Ah ok, vielen Dank.
>
>
> Beim scharfen Hinsehen auf millis() viel mir auf, dass man um eine
> Zwischenspeicherung nicht herrumkommt:
>
> * timer0_millis ist long -> beim Zugriff kann einem ein ISR dazwischen
> pfuschen
> * => SREG save&restore nötig
> * return timer0_millis geht hier nicht, da sonst SREG = oldSREG; fehlen
> würde
>
> -> also wird es in millis() nicht aus Übervorsicht, oder aus Performanz
> sondern, wie spacedog bereits bemerkte wgn der atomität bzw. weil es
> nicht anders geht ;)

Hmm... ich verstehe deinen Gedankengang nicht ganz. Meiner Meinung nach 
wirfst du verschiedene Dinge in einen Topf.
Zur SREG-Verwendung sei auf
http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Programmieren_mit_Interrupts
verwiesen. Das hat nix mit ULONG oder volatile zu tun.

Die Verwendung von millis() ist auch ohne 'volatile' notwendig, weil 
eben ein Interrupt dazwischen funken kann.

Volatile wird nur verwendet, um sicher zu stellen, dass der Compiler 
keine Zugriffsoptimierungen bei dieser Variablen macht. Im Grunde 
genommen braucht mandas bei jeder Variablen, die in einem Interrupt 
geändert werden kann. Das kann der Compilernämlich nicht vorhersehen.

von Robert F. (fastred)


Lesenswert?

Daniel V. schrieb:
[..]
>
> Hmm... ich verstehe deinen Gedankengang nicht ganz. Meiner Meinung nach
> wirfst du verschiedene Dinge in einen Topf.
> Zur SREG-Verwendung sei auf
> 
http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Programmieren_mit_Interrupts
> verwiesen. Das hat nix mit ULONG oder volatile zu tun.
> [...]

Ich habe mich in der Tat unklar ausgedrückt, sry.

Klar hat das Statusregister nichts mit long oder volatile zutun ;)

Vieleicht sollte ich das laut-denken im Forum unterlassen - mein letztes 
Post bitte überlesen ;)

von Peter D. (peda)


Lesenswert?

Zwischenvariablen im Interrupt machen den Code auch nicht gerade besser 
lesbar.

Es gibt aber einen Trick, ohne volatile auszukommen.
Man macht den Zugriff auf Main-Level mit folgenden Macros:
1
#define  vu8(x) (*(volatile  uint8_t*)&(x))
2
#define  vs8(x) (*(volatile   int8_t*)&(x))
3
#define vu16(x) (*(volatile uint16_t*)&(x))
4
#define vs16(x) (*(volatile  int16_t*)&(x))
5
#define vu32(x) (*(volatile uint32_t*)&(x))
6
#define vs32(x) (*(volatile  int32_t*)&(x))

Der Trick dabei, eine Variable läßt sich nicht auf volatile casten, ein 
Pointer aber schon.
Das hängt wohl damit zusammen, daß das volatile erst nachträglich in C 
reingestrickt wurde, also kein richtiger Typ ist.
Die C-Erfinder hatten es nicht so mit Hardware naher Programmierung.


Bei mehrbytigen Zugriffen muß man außerdem noch atomar kapseln.
Bei Verwendung des ATOMIC-Macros könnte man auch das volatile ganz 
weglassen, der Zugriff wird nicht wegoptimiert.
Allerdings kann dann Code von außerhalb mit in das ATOMIC-Macro 
hineingezogen werden. Bei einer Division sind dann also Interrupts für 
lange Zeit gesperrt.


Der Vernachlässigung der volatile und atomic Problematik ist bestimmt 
für 90% der Bluescreens und Abstürze verantwortlich.


Peter

von Peter (Gast)


Lesenswert?

Peter Dannegger schrieb:
> Der Trick dabei, eine Variable läßt sich nicht auf volatile casten, ein
> Pointer aber schon.

Danke soetwas hatte ich schon lange mal gesucht.

von Rolf Magnus (Gast)


Lesenswert?

Peter Dannegger schrieb:

> Der Trick dabei, eine Variable läßt sich nicht auf volatile casten, ein
> Pointer aber schon.
> Das hängt wohl damit zusammen, daß das volatile erst nachträglich in C
> reingestrickt wurde, also kein richtiger Typ ist.

Eher damit, daß ein Cast niemals irgendwas am Original verändert, 
sondern immer eine (temporäre) Kopie erzeugt, die dann den Zieltyp hat. 
Das bringt einem aber natürlich nichts, wenn man auf die 
Original-Variable zugreifen will, also nimmt man einen Zeiger darauf und 
castet den. Mit volatile hat das nichts zu tun, es gilt vielmehr für 
jeden Cast, wo man ein Objekt irgendeines Typs als anderen Typ 
interpretieren will. Da man nicht auf eine Kopie, sondern auf das 
Original-Objekt zugreifen will, muß man seine Adresse nehmen und die 
casten.

Übrigens:
Ich finde es aber eher besser, die Variable generell volatile zu machen 
und dann mit der lokalen Variablen zu arbeiten, wenn mehrere Zugriffe 
nötig sind.
Das finde ich klarer, als irgendwo vereinzelte volatile-Zugriffe 
reinzufummeln. Und ich bin auch sicher, daß mein Zugriff nur einmal 
stattfindet. Bei deiner Variante bin ich darauf angewiesen, daß der 
Compiler die anderen Zugriffe wegoptimiert.

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.