Ich treffe ab und zu mal auf irgendwelche NERDS die mit solchen Sprüchen
kommen: "mit volatile sagt man dem Compiler, dass er nicht optimieren
darf".
Dann denk ich mir: "Der hat lange nicht mehr programmiert"
(1) volatile = flüchtig
(2) Die betreffende Variable kann beispielsweise im Speicher oder in
Memory-mapped Registern liegen (z.B. SPI-Controller, RX-Register)
(3) Die Variable kann sich von einer externen Quelle ändern.
(4) Wenn sich die Variable zwischen zwei Befehlen an ihrer Quelle ändern
kann, dann darf der Compiler nicht auf einer lokalen Kopie in einem
CPU-Register arbeiten:
So wird aus x++; x*=2; in PseudoCode:
LDR x
INC x
STR x
LDR x
SHIFT x,1
STR x
anstatt:
LDR x
INC x
SHIFT x,1
STR x
(5) volatile heißt nicht zwangsweise das Zugriffe darauf atomar sind.
Der Satz: "volatile verwendest du bei Variablen die du in einer
Interrupt-Routine und im Programm verwendest" ist oberflächlich.
Denn der Befehl x++; ist nicht atomar.
Er besteht aus LDR, INC,STR. (Read, Modify, Write)
Nun kommt der Interrupt direct nach LDR rein und setzt die variable im
Speicher auf (beispielsweise) Null. Direkt nach der Rückkehr wird aber
noch der alte Wert inkrementiert und zurückgeschrieben.
Deswegen schreibt man variablen mit multiplen zugriff auch in einer
nicht-unterbrechbaren Interrupt-Routine oder deaktiviert Interrupts für
den Schreibzugriff.
Nun zu meiner Frage:
Die Sequenz
volatile int a,x,y,z,
a = x * y + z;
kann doch optimiert werden?
LDR x
LDR y
LDR z
MADD x,y,z
STR a
Der Befehl heißt Multiply-Add.
Gerd schrieb:> Der Satz: "volatile verwendest du bei Variablen die du in einer> Interrupt-Routine und im Programm verwendest" ist oberflächlich.
das Obige schließt das Folgende nicht aus:
> Denn der Befehl x++; ist nicht atomar.
atomar bzw. nicht atomar kann ein Problem sein, volatile
ist auch bei sonst atomaren Zugriffen manchmal nötig.
Wer also sagt, daß irgendwo volatile nötig ist, hat sich zu
atomar überhaupt nicht geäußert.
Die obige Nerd-Aussage war natürlich etwas knapp formuliert.
Entscheidend ist, was optimiert wird. Bei einer volatile Variablen
darf der Compiler Zugriffe darauf nicht wegoptimieren und diese Zugriffe
müssen innerhalb der entsprechenden sequence points stattfinden. Das
wars dann aber auch schon. Der geladene Wert selbst ist nicht mehr
volatile.
Freilich sollte der Zugriff auf die Variable in der Form erfolgen, wie
er sich aus dem Programmtext ergibt. Wenn das also eine 32-Bit Variable
ist, dann hat der Compiler die verdammte Pflicht, einen 32-Bit
Lade/Speicherbefehl dafür zu verwenden (mal angenommen er hat einen).
Auch wenn man nur ein Byte davon benötigt (in diese Falle tappen
Compiler manchmal), oder man zweckmässigerweise gleich zwei zusammen in
einem 64-Bit Befehl laden könnte. Das hat natürlich Folgen für SIMD
Befehle.
Technischer Hintergrund: Spezielle Speichersysteme und Steuerregister
können bestimmte Zugriffsbreiten erfordern, so dass beispielsweise ein
Bytezugriff nicht zulässig ist.
Gerd schrieb:> Denn der Befehl x++; ist nicht atomar.
Da Du nicht eine bestimmte CPU genannt hast, ist diese Aussage falsch.
Z.B. der 8051 kann sehr wohl im internen SRAM "INC address" ausführen
(auch DEC, XRL, ANL, ORL, DJNZ).
Und man kann sogar eine Variable atomar lesen und löschen (XCH).
Und wie schon gesagt wurde, volatile und atomar sind verschiedene Dinge.
Sie werden nur oft zusammen benötigt.
Peter
Hatte mal auf eine volatile variable aus einer Interrupt-Routine und der
main aufgerufen. (UART-Treiber). Zugriffe waren nicht atomar. Die
Wahrscheinlichkeit, dass das es zu einem Fehler kam lag bei etwa
1:10000000 verarbeiteten Bytes. Was dann das Programm abschmieren ließ.
(Anstatt main+Int könnten das auch zwei threads sein)
Ein Testlauf gerät so zum Lottospiel. Man weiß ja vorher nicht, wieviele
Daten man übertragen muss um auszuschließen, dass der Code funktioniert.
10000 Bytes? 1 Mio Bytes? 1GByte?
Dann sieht man bei gestandenen Programmieren mit 'jahrelanger' Erfahrung
solche Fehler...
Mit der Implementierung von volatile bin ich auch etwas unglücklich. Man
kann es nicht wie andere Typen casten, sondern es ist für eine Variable
immer generell.
D.h. auch im Interrupthandler wird damit die Optimierung ausgeschaltet,
also da, wo es am meisten wehtut.
Peter
Die Sequenz:
volatile i;
...
if ((i-1)!=0) dosomething();
wird nicht mit (Pseudocode)
LDR i
dec i
cmp i,0
bz dosomething
sondern zu
LDR i
DJNZ dosomething
übersetzt?
Okay das Ergebnis von i-1 ist ja nicht mehr volatile und kann in einem
Register gehalten werden? Also kann dort auch optimiert werden?
Peter Dannegger schrieb:> Mit der Implementierung von volatile bin ich auch etwas unglücklich. Man> kann es nicht wie andere Typen casten, sondern es ist für eine Variable> immer generell.
Die Aussage ist so ziemlich falsch.
Natürlich kann man volatile casten. Und genau das kann man verwenden
damit der compiler besseren Code erzeugen kann.
Volatile ansich ist eine ziemlich undefinierte Sache. Es ist quasi
Compilerabhängig was wirklich passiert.
Und selbst dann bringt es einem genau überhaupt keine Garantien über
Sachen die zur Laufzeit passieren, wie runtime reordering. Das ist
natürlich auf dem AVR kein Problem, weil es dort sowas nicht gibt.
Ich verwende in meinen Programmen volatile überhaupt nicht, sondern
explizite memory-barriers. Eine barrier kann man beispielsweise so
definieren:
Diese barrier funktioniert auf CPUs, die kein runtime reordering haben.
Wie man memory barriers verwendet kann man hier ganz gut nachlesen:
http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=blob_plain;f=Documentation/memory-barriers.txt;hb=HEAD
(Das Dokument beschreibt natürlich die ganze Geschichte. Mit CPUs die
runtime reordering und SMP können. In der Situation würde volatile
alleine komplett versagen.)
Mit der Verwendung von barriers hat der compiler wesentlich mehr
Möglichkeiten den Code zu optimieren. Es ist halt nur auch komplizierter
damit korrekten Code zu schreiben. Man muss halt erstmal verstehen wie
das ganze funktioniert.
Edit meint: Bevor jemand meint ich hätte ja doch volatile in mb()
verwendet: In gcc inline asm hat volatile eine andere und ganz klar
definierte semantik.
Michael Buesch schrieb:> Die Aussage ist so ziemlich falsch.> Natürlich kann man volatile casten.
Kannst Du uns auch verraten, wie?
Beim AVR-GCC funktioniert es nicht.
Bzw. nur über Macros von hinten durch die Brust ins Auge:
Peter Dannegger schrieb:> Michael Buesch schrieb:>> Die Aussage ist so ziemlich falsch.>> Natürlich kann man volatile casten.>> Kannst Du uns auch verraten, wie?> Beim AVR-GCC funktioniert es nicht.>> Bzw. nur über Macros von hinten durch die Brust ins Auge:
Das Macros nur Textersetzung sind weißt du bestimmt.
volatile ist nur ein qualifier wie auch const. Das heißt man kann
volatile genauso casten wie const.
Natürlich ergibt nicht jede cast möglichkeit korrekten Code. Aber das
hat nichts mit volatile zu tun.
Projekte wie der Linux Kernel (und auch meine) treiben das Konzept auf
die Spitze. Volatile wird dort nur über casts verwendet. Du wirst kein
Datum im Kernel finden welches als volatile deklariert ist (außer
vielleicht in 15 Jahre altem Code, den niemand mehr anfassen will. Wie
beispielsweise einige elemente in diversen TTY strukturen, task_struct
und 32bit-jiffies.).
Generell wird aber versucht zu vermeiden volatile überhaupt zu
verwenden, weil die richtige Antwort oft nicht volatile sondern memory
barrier heißt.
Michael Buesch schrieb:> volatile ist nur ein qualifier wie auch const. Das heißt man kann> volatile genauso casten wie const.
Und warum geht es dann im AVR-GCC nicht?
Peter
Peter Dannegger schrieb:> Michael Buesch schrieb:>> volatile ist nur ein qualifier wie auch const. Das heißt man kann>> volatile genauso casten wie const.>> Und warum geht es dann im AVR-GCC nicht?
Wtf? Du hast doch oben selbst geschrieben das es geht.
Außerdem verwendet die libc es in den I/O Definitionen (was auch korrekt
ist).
Michael Buesch schrieb:> Wtf? Du hast doch oben selbst geschrieben das es geht.
Ich meinte natürlich, nicht so super umständlich über Pointer.
Da muß man ja für jeden Variablentyp ein extra Macro schreiben.
Wenn man dann noch Arrays, Unions und Structs hat, wird das ganz schön
haarig.
Und nicht überall, wo man eine Variable einsetzen kann, kann man auch
ein Macro verwenden.
Peter
Peter Dannegger schrieb:> Michael Buesch schrieb:>> Wtf? Du hast doch oben selbst geschrieben das es geht.>> Ich meinte natürlich, nicht so super umständlich über Pointer.> Da muß man ja für jeden Variablentyp ein extra Macro schreiben.> Wenn man dann noch Arrays, Unions und Structs hat, wird das ganz schön> haarig.
nicht, wenn man typeof nimmt.
Peter Dannegger schrieb:> Michael Buesch schrieb:>> Wtf? Du hast doch oben selbst geschrieben das es geht.>> Ich meinte natürlich, nicht so super umständlich über Pointer.> Da muß man ja für jeden Variablentyp ein extra Macro schreiben.> Wenn man dann noch Arrays, Unions und Structs hat, wird das ganz schön> haarig.
Da gibts doch gar nichts "super umständliches" dran. Das ist ganz
einfaches C. So einen cast sollte jeder lesen und schreiben und
verstehen können, der behauptet C zu können.
Und was wird an arrays, unions und structs haarig? Gerade bei arrays
gibts nichts haariges, wegen der array/pointer dualität.
Außerdem gibts typeof, wie schon gesagt, mit dem man sehr tolle makros
machen kann, wenn man es denn unbedingt will.
> Und nicht überall, wo man eine Variable einsetzen kann, kann man auch> ein Macro verwenden.
Ein Beispiel würde mich sehr interessieren.
Peter Dannegger schrieb:> Mit der Implementierung von volatile bin ich auch etwas unglücklich. Man> kann es nicht wie andere Typen casten, sondern es ist für eine Variable> immer generell.> D.h. auch im Interrupthandler wird damit die Optimierung ausgeschaltet,> also da, wo es am meisten wehtut.
Dazu kopiert man sie doch einfach am Anfang der ISR in eine lokale
Variable und die dann am Ende zurück.
Michael Buesch schrieb:> Außerdem gibts typeof, wie schon gesagt, mit dem man sehr tolle makros> machen kann, wenn man es denn unbedingt will.
Das ist aber kein Standard-C, gibt's also nur auf manchen Compilern.
In C++ könnte man es elegaant und einfach über ein Template lösen, etwa
so:
Rolf Magnus schrieb:> Das ist aber kein Standard-C, gibt's also nur auf manchen Compilern.
Stimmt, aber nachdem Programme dieser Art ohnehin kaum portabel
sind, kann man das meistens vertreten, denke ich.
Meine AVR-Programme werden nie auf etwas anderem als dem gcc
laufen, ebenso alles, was ich mit Threads unter Linux schreibe.
> In C++ könnte man es elegaant und einfach über ein Template lösen
Das ist richtig und ich verstehe auch nicht, wieso man nicht
einfach C++ nutzt, wo es nichts schadet.
Es hat an vielen Stellen Vorteile, wo man sich in C ziemlich
verbiegen muß.
Michael Buesch schrieb:> Außerdem gibts typeof, wie schon gesagt, mit dem man sehr tolle makros> machen kann, wenn man es denn unbedingt will.
Danke, funktionert. Jetzt brauche ich nur noch ein Macro.
typeof ist ja dann auch bequem, um Pointer anzulegen.
> Ein Beispiel würde mich sehr interessieren.
Habe ich mir leider nicht gemerkt.
Die Fehlermeldung könnte "lvalue required" gewesen sein.
Peter
Rolf Magnus schrieb:> Das ist aber kein Standard-C, gibt's also nur auf manchen Compilern.
Jo ganz klar.
Es soll aber auch gesagt sein, dass volatile an sich auch nicht so
portabel ist wie das viele denken. Der C Standard macht nur sehr
ungenaue Aussagen über die Funktion von volatile im Zusammenhang mit
Daten.
Die meisten Compiler werden es aber als "schalte so gut wie alle
Optimierungen für dieses Datum ab" übersetzen, aber Laufzeiteffekte
(runtime reordering, SMP) ignorieren.
Peter Dannegger schrieb:> Michael Buesch schrieb:>> Außerdem gibts typeof, wie schon gesagt, mit dem man sehr tolle makros>> machen kann, wenn man es denn unbedingt will.>> Danke, funktionert. Jetzt brauche ich nur noch ein Macro.> typeof ist ja dann auch bequem, um Pointer anzulegen.
Nebenbei sei erwähnt, dass typeof das Makroargument nicht evaluiert. Es
also keine Nebenwirkungen gibt. Also das wäre eine einmal-evaluation:
#define foo(bar) typeof(bar) x = (bar)
Das wäre zweimal:
#define foo(bar) typeof(bar) x = (bar) + (bar)
Je nachdem was "bar" ist und welche Nebenwirkungen es hat (Register oder
komplexes statement), kann das natürlich fatal sein. Deshalb gibts diese
Ausnahme für typeof.
>> Ein Beispiel würde mich sehr interessieren.>> Habe ich mir leider nicht gemerkt.> Die Fehlermeldung könnte "lvalue required" gewesen sein.
Ja ok. Das kommt natürlich auch immer auf das Makro und die Verwendung
an. Generell sollte es aber keine Probleme geben. Wenn man die Adresse
des Makros (also des Makroinhalts) im Code nimmt, kann es zu Problemen
kommen. Aber ich denke es wäre ohnehin ein Programmierfehler dies zu
machen, weil diese Makros es ja eben verhindern wollen, dass der
Anwender sich mit den Pointern befasst.
Michael Buesch schrieb:> Volatile ansich ist eine ziemlich undefinierte Sache. Es ist quasi> Compilerabhängig was wirklich passiert.
Das Verhalten des Compilers bei volatile Variablen ist meist im ABI
der Zielarchitektur definiert.
> Mit der Verwendung von barriers hat der compiler wesentlich mehr> Möglichkeiten den Code zu optimieren.
Memory barriers ersetzen aber volatile nicht. Oft müssen sogar beide
Konstrukte gemeinsam genutzt werden.
Wie A.K. in Beitrag "Re: Wie man volatile optimiert" schon
am Rande andeutete, hat volatile einen Effekt, der meist übersehen
wird: Es verhindert eben auch, dass der Compiler un-optimiert. Bei
einer regulären Variablen ist es nicht festgelegt, dass der Compiler
den "besten" Speicherzugriff erzeugt. Er könnte einen 32bit Wert
problemlos durch vier einzelne Bytezugriffe laden. Genau das
verhindert volatile in den mir bekannten Fällen. Beispielsweise wird
gefordert, dass ein 32bit Wert mit genau einem 32bit breiten
Speicherzugriff geladen wird.
Das run-time reordering musst Du mit entsprechenden page-tables
verhindern, oder in Einzelfällen eben manuell durch memory barriers.
Gruß
Marcus
Marcus Harnisch schrieb:> Memory barriers ersetzen aber volatile nicht. Oft müssen sogar beide> Konstrukte gemeinsam genutzt werden.>> Wie A.K. in Beitrag "Re: Wie man volatile optimiert" schon> am Rande andeutete, hat volatile einen Effekt, der meist übersehen> wird: Es verhindert eben auch, dass der Compiler un-optimiert. Bei> einer regulären Variablen ist es nicht festgelegt, dass der Compiler> den "besten" Speicherzugriff erzeugt. Er könnte einen 32bit Wert> problemlos durch vier einzelne Bytezugriffe laden. Genau das> verhindert volatile in den mir bekannten Fällen. Beispielsweise wird> gefordert, dass ein 32bit Wert mit genau einem 32bit breiten> Speicherzugriff geladen wird.
Das wird nur bei memory mapped I/O gebraucht. Und genau dort (und
eigentlich auch nur dort) ist volatile sinnvoll. Es wird aber auch
vollständig von den I/O Wrapper macros verdeckt. Deshalb kann ich davon
sprechen kein volatile in dem Code zu verwenden.
> Das run-time reordering musst Du mit entsprechenden page-tables> verhindern, oder in Einzelfällen eben manuell durch memory barriers.
Was sollen page tables denn damit zu tun haben? MBs sind keine
Einzelfälle, sondern die Regel um Reordering zu verhindern.
Michael Buesch schrieb:> Was sollen page tables denn damit zu tun haben? MBs sind keine> Einzelfälle, sondern die Regel um Reordering zu verhindern.
Einige Architekturen definieren mittels Page Tables sogenannte memorytypes (der Begriff kann sich natürlich je nach Hersteller
unterscheiden). Diese memory types legen unter anderem fest, ob
aufeinanderfolgende Zugriffe innerhalb der Page oder zwischen Pages
verschiedenen Typs optimiert werden dürfen (reordering, store merging,
etc). Siehe z.B. die ARMv6, bzw. ARMv7 VMSA.
Gruß
Marcus