Forum: Compiler & IDEs Pointer-Qualifier Verständnis Problem


von Fabian B. (fabs)


Lesenswert?

Hallo C-Pros,
 ich habe ein Array mit volatile Structs:
1
typedef volatile struct{
2
  U8         handle; 
3
  can_cmd_t  cmd; 
4
  can_id_t   id;
5
  can_msk_t  msk;           //- Hinzugefügt
6
  U8         dlc;  
7
  U8         data[8]; 
8
  U8         status; 
9
  can_ctrl_t ctrl;   
10
} st_cmd_t;
11
12
st_cmd_t messageRb[MAX_BUFFER];

Wie man sieht, enthält das Struct ein U8 Array. Jetzt habe ich eine 
Fuktion, die erwartet als Parameter einen U8*, aber da in C ja Array und 
Ptr sehr ähnlich sind, müsste ich doch die Startadresse des Arrays auch 
dieser Funktion übergeben können:

Funktion:
1
void can_get_data(U8* p_can_message_data)
Aufruf:
1
can_get_data((&cmd->data[0]));

Nu wirft mir der Compiler aber:
1
warning: passing argument 1 of 'can_get_data' discards qualifiers from pointer target type

Nach meinem Verständnis kommt das, da das Struct volatile ist und der 
U8* der Funktion nicht. Da wärend diesem Aufruf aber keine Interrupts 
zwischenhauen können, dürfte das doch aber kein Problem sein, das 
Warning durch Ändern des Aufrufs in
1
can_get_data((U8*)(&cmd->data[0]));
zu beseitigen...

Stimmt meine Annahme, oder bin ich total auf dem Holzweg?

Gruß
Fabian

von J. V. (janvi)


Lesenswert?

bissel weiter unten in dem volatile thread habe ich micht vorgestern 
über das Gleiche gewundert. GCC schmeisst bei einer solchen Warnung aber 
nicht nur die volatile Eigenschaft sondern möglicherweise auch noch 
andere Eigenschaften der Variable weg (bei mir wars signed - und schon 
tuts nicht mehr). Wenn volatile, dann also sowohl bei der Deklaration 
als auch bei den by Reference Parametern ranschreiben oder ganz 
weglassen wenn es nicht gebraucht wird. Nur ranschreiben weils nicht 
schadet könnte falsch sein. Also:

void can_get_data(volatile U8* p_can_message_data)

von Klaus W. (mfgkw)


Lesenswert?

Wobei ich mir nicht so recht sicher bin, ob das volatile überhaupt
in das typedef für die struct gehört.
Rein intuitiv würde ich es eher einem Objekt im Speicher gönnen,
also im typedef weglassen und bei der Variablendeklaration
dafür dran schreiben - oder eben nicht.
Tur hier zwar nichts zur Sache, aber fiel mir nur so auf.

von J. V. (janvi)


Lesenswert?

> Da wärend diesem Aufruf aber keine Interrupts zwischenhauen können,

hat mit dem gar nichts zu tun. Die Optimierer sehen aber, wenn eine 
nicht volatile Variable immer nur gelesen wird und niemand anderes 
(zwischendurch) was reinschreibt. Also kommen sie auf die Idee, den 
Lesezugriff wegzuoptimieren und durch eine Konstante oder das was vom 
letzen Durchgang noch in einem Register steht oder anderen Quatsch zu 
ersetzen weil das kürzer scheint.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Fabian B. schrieb:

> Nach meinem Verständnis kommt das, da das Struct volatile ist und der
> U8* der Funktion nicht.

Ja.

> Da wärend diesem Aufruf aber keine Interrupts
> zwischenhauen können, dürfte das doch aber kein Problem sein, das
> Warning durch Ändern des Aufrufs in
>
1
> can_get_data((U8*)(&cmd->data[0]));
2
>
> zu beseitigen...

Wenn's nicht volatile sein muss, warum deklarierst du es dann
volatile?

von (prx) A. K. (prx)


Lesenswert?

J. V. schrieb:

> GCC schmeisst bei einer solchen Warnung aber
> nicht nur die volatile Eigenschaft sondern möglicherweise auch noch
> andere Eigenschaften der Variable weg (bei mir wars signed - und schon
> tuts nicht mehr).

Ich dachte diese unsinnige Aussage hatten wir geklärt.

von (prx) A. K. (prx)


Lesenswert?

Fabian B. schrieb:

> Stimmt meine Annahme, oder bin ich total auf dem Holzweg?

Nein, das siehst du richtig.

Wobei man
  can_get_data((U8*)(&cmd->data[0]));
auch weniger verkrampft als
  can_get_data((U8*)cmd->data);
schreiben kann.

von Fabian B. (fabs)


Lesenswert?

@all: danke erstmal!
 die Struct selbst muss schon volatile sein, da sie sowohl im 
Hauptprogramm als auch in Interrupts bearbeitet wird, bzw. (genau weil 
oben der Optimierer ja bereits angesprochen wurde) im Hauptprog nur 
gelesen und im Int gefüllt wird.

Pointer fasse ich halt immer noch mit der Kneifzange an...ich hab da 
jetzt so viel drüber gelesen, aber so ganz geheuer sind sie mir noch 
nicht.

Gruß
Fabian

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Fabian B. schrieb:

>  die Struct selbst muss schon volatile sein, da sie sowohl im
> Hauptprogramm als auch in Interrupts bearbeitet wird, bzw. (genau weil
> oben der Optimierer ja bereits angesprochen wurde) im Hauptprog nur
> gelesen und im Int gefüllt wird.

Deshalb muss sie noch lange nicht im Ganzen volatile sein.  Es
genügt völlig, nur die Elemente so zu deklarieren, für die das
notwendig ist.  Man kann sogar noch einen Schritt weitergehen und
per typecast das volatile nur dann hinzu fügen, wenn es benötigt
wird.

von Fabian B. (fabs)


Lesenswert?

Kannst du das noch etwas ausführen?
Es ist so, dass alle Elemente des Struct im Interrupt verändert werden 
können. Daher müssen doch alle Elemente auch Volatile sein, oder? Und am 
einfachsten mache ich das doch, indem ich die ganze Struct Volatile 
qualifiziere...

Gruß
Fabian

von Karl H. (kbuchegg)


Lesenswert?

Fabian B. schrieb:
> @all: danke erstmal!
>  die Struct selbst muss schon volatile sein,

So wie du das geschrieben hast, muss die struct Vereinbarung keineswegs 
volatile sein.

Was du hier hast:
1
typedef volatile struct{
2
  U8         handle; 
3
  can_cmd_t  cmd; 
4
  can_id_t   id;
5
  can_msk_t  msk;           //- Hinzugefügt
6
  U8         dlc;  
7
  U8         data[8]; 
8
  U8         status; 
9
  can_ctrl_t ctrl;   
10
} st_cmd_t;

ist nur eine Blaupause. Eine Anleitung, wie eine Variable von diesem Typ 
aussehen würde, wenn man eine erzeugen würde. Es ist so wie ein Bauplan 
eines Hauses. Schreibst du nun in diesen Plan hinein, dass die Haustür 
aus Schmiedeeisen sein soll, dann erhalten alle Häuser, die nach 
diesem Plan gebaut werden eine schmiedeeisene Tür. Egal ob der 
tatsächliche Besitzer eines ganz bestimmten Hauses das so haben möchte 
oder nicht. Der Plan schreibt bereits vor, dass das so sein muss und bei 
der tatsächlichen Bauausführung gibt es keine Möglichkeit mehr von 
diesem Plan abzuweichen.

Was du willst ist, dass deine Variable messageRb volatile ist. Und nur 
diese! Es mag aber irgendwann durchaus auch andere st_cmd_t Variablen 
geben, die nicht volatile sind. Es ist daher mehr als unklug, bereits in 
der Strukturbeschreibung zu fordern, dass alle Variablen die jemals vom 
Typ st_cmd_t erzeugt werden, volatile sein müssen.
1
typedef struct{
2
  U8         handle; 
3
  can_cmd_t  cmd; 
4
  can_id_t   id;
5
  can_msk_t  msk;           //- Hinzugefügt
6
  U8         dlc;  
7
  U8         data[8]; 
8
  U8         status; 
9
  can_ctrl_t ctrl;   
10
} st_cmd_t;
11
12
volatile st_cmd_t messageRb[MAX_BUFFER];

Jetzt ist nur messageRb volatile. Du hast aber nach wie vor die 
Möglichkeit auch Variablen vom Type st_cmd_t zu erzeugen, die nicht 
volatile sind.

In einem Bauplan baut man sich keine Restriktionen ein, die nicht 
unbedingt sein müssen. Ansonsten hat man bei der Bauausführung keinen 
Spielraum mehr.

von Fabian B. (fabs)


Lesenswert?

Deine anschaulichen Beispiele find ich immer klasse, Karl Heinz.

Es ist sicher sinnvoll das so zu ändern, werde ich auch tun. Gäbe es 
jedoch nur genau ein "Haus nach diesem Bauplan" würde es jedoch keinen 
Unterschied machen, richtig? Auch wenn man sich dann sicherlich den 
ganzen typedef sparen könnte...

Gruß
Fabian

von Karl H. (kbuchegg)


Lesenswert?

Fabian B. schrieb:
> Deine anschaulichen Beispiele find ich immer klasse, Karl Heinz.
>
> Es ist sicher sinnvoll das so zu ändern, werde ich auch tun. Gäbe es
> jedoch nur genau ein "Haus nach diesem Bauplan" würde es jedoch keinen
> Unterschied machen, richtig? Auch wenn man sich dann sicherlich den
> ganzen typedef sparen könnte...

Irgendwo braucht man den Typ immer wieder mal. Und sei es nur in einer 
Helperfunktion, die irgendeine Operation auf einer Variablen dieses 
Types macht.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Fabian B. schrieb:

> Es ist so, dass alle Elemente des Struct im Interrupt verändert werden
> können. Daher müssen doch alle Elemente auch Volatile sein, oder?

Nein, das ist ein weit verbreiteter Irrtum.

volatile müssen erst einmal nur solche Objekte sein, bei denen der
Compiler die Änderung sonst nicht erwarten würde, weil sie aus
seiner Sicht im normalen Codefluss nicht erfolgen kann.  Oft genug
ist es völlig ausreichend, ein einzelnes Flag als volatile zu
markieren, in einem der Threads auf dieses Flag zu pollen, und erst
danach dann die mit diesem Flag geschützten Daten auszulesen.  Wenn
an dieser Stelle die Möglichkeit besteht, dass ein externer Seiten-
effekt die weiteren Daten modifiziert haben könnte (beispielsweise
ein Aufruf einer externen Funktion), dann ist der Compiler veranlasst,
die entsprechenden Daten hier einzulesen, da er sich nicht mehr auf
eine im einem Register o. ä. gecachete Kopie verlassen darf.

volatile ist eine so schlimme Pessimierung, dass man sie nicht als
,,prinzipiellen Rundumschlag, um auf der sicheren Seite zu sein''
benutzen sollte.  Man sollte sich stattdessen überlegen, wo es wirklich
notwendig ist.  Wenn man die Daten (mehr als nur ein gepolltes Flag)
schon volatile hat, dann muss man sich (um den Pessimierungseffekt zu
minimieren) andernfalls die Mühe machen, von den volatile-Daten dann
,,Arbeitskopien'' anzufertigen und auf diesen zu arbeiten.

Ein Indiz für die Überflüssigkeit von volatile ist es auch, dass einen
der Compiler warnt, dass er das volatile (wie hier) verworfen hat,
der Code aber dennoch sauber funktioniert.

von (prx) A. K. (prx)


Lesenswert?

Wie ist das eigentlich bei volatile genau definiert, wenn entsprechend 
dem von dir skizzierten Szenario von mehreren Variablen nur eine 
volatile ist. Stellt der Zugriff auf diese eine Variable tatsächlich 
eine Art memory barrier dar, derzufolge auch alle Kopien anderer 
Variablen beliebigen Datentyps ungültig sind?

von Fabian B. (fabs)


Lesenswert?

@Jörg: Danke für die Ausführungen, ich bin mir aber nicht sicher ob ich 
das richtig verstanden habe:

In meinem Fall habe ich ein Array aus der besagten Struktur, das quasi 
als primitiver Ringpuffer her hält. Dieser Ringpuffer wird im Interrupt 
gefüllt.
Im Hauptprogramm hole ich mir ein Element aus der Puffer, und verarbeite 
den Inhalt. Hier wird in den RP aber nichts geschrieben.

Da die Funktion, die aus dem Ringpuffer liest ja nichts von dem 
Interrupt "weiss", muss der Ringpuffer doch als volatile qualifiziert 
werden, richtig?

Sollte ich jetzt um die "Pessimierung" zu minimieren beim Lesen aus dem 
Ringpuffer im Hauptprogramm lieber eine Kopie des Objekts machen und 
dann mit diesem weiter arbeiten? Ist es das was du meinst?

Und das mit dem Flag: meinst du, der Compiler wäre gezwungen, das 
gesamte Objekt neu einzulesen (also evtl gecachete Inhalte zu verwerfen) 
wenn das Struct zwar nicht volatile im Ganzen jedoch ein Flag im Struct 
volatile qualifiziert wird und man dieses Flag ausserhalb des Interrupts 
zuerst abfragt?

Gruß
Fabian

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

A. K. schrieb:

> Wie ist das eigentlich bei volatile genau definiert, wenn entsprechend
> dem von dir skizzierten Szenario von mehreren Variablen nur eine
> volatile ist. Stellt der Zugriff auf diese eine Variable tatsächlich
> eine Art memory barrier dar, derzufolge auch alle Kopien anderer
> Variablen beliebigen Datentyps ungültig sind?

Nein, der Zugriff auf eine Variable macht das nicht, aber ein
Aufruf einer externen Funktion impliziert die Möglichkeit, dass
diese Funktion auch globale Daten ändert.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Fabian B. schrieb:

> Da die Funktion, die aus dem Ringpuffer liest ja nichts von dem
> Interrupt "weiss", muss der Ringpuffer doch als volatile qualifiziert
> werden, richtig?

Nur, wenn sie ansonsten zu der Annahme gelangen könnte, dass die
Daten darin sich nicht geändert hätten und sie die Möglichkeit hat,
noch auf ältere (gecachete) Daten zurückzugreifen.  Wenn diese
Möglichkeit ohnehin nicht besteht, ist das volatile entbehrlich.

Normalerweise braucht man volatile wirklich nur für Dinge, die
man pollt, bis sie ihre Werte ändern.  Das kann ein Flag sein, es
kann aber auch der Zeiger eines Ringpuffers sein, auf dessen Änderung
man wartet.  Vorsicht aber, dass man sich bei Zeigern keine Probleme
mit nicht-atomaren Zugriffen einhandelt (wenn sie größer als 8 bit
sind).

von Fabian B. (fabs)


Lesenswert?

Jörg Wunsch schrieb:
> Fabian B. schrieb:
>
>> Da die Funktion, die aus dem Ringpuffer liest ja nichts von dem
>> Interrupt "weiss", muss der Ringpuffer doch als volatile qualifiziert
>> werden, richtig?
>
> Nur, wenn sie ansonsten zu der Annahme gelangen könnte, dass die
> Daten darin sich nicht geändert hätten und sie die Möglichkeit hat,
> noch auf ältere (gecachete) Daten zurückzugreifen.  Wenn diese
> Möglichkeit ohnehin nicht besteht, ist das volatile entbehrlich.

Wie kann ich das zuverlässig erkennen...mir fehlt da der Blick (oder die 
Einsicht ;-) ) zu.

>
> Normalerweise braucht man volatile wirklich nur für Dinge, die
> man pollt, bis sie ihre Werte ändern.  Das kann ein Flag sein, es
> kann aber auch der Zeiger eines Ringpuffers sein, auf dessen Änderung
> man wartet.  Vorsicht aber, dass man sich bei Zeigern keine Probleme
> mit nicht-atomaren Zugriffen einhandelt (wenn sie größer als 8 bit
> sind).

Wenn ich also meinen Ringpuffer nicht mehr volatile qualifiziere, die 
Zeiger (in meinem Fall einfache Indexvariablen) aber weiterhin, und ich 
in meinem Hauptprogramm die "readRB"-Funktion aufrufe (die als extern in 
einem anderen Modul bekannt ist) müsste es reichen oder?
Oder nur wenn die readRB-Funktion tatsächlich eine Kopie des Objektes 
zurückliefert und nicht zur einen Zeiger auf den aktuellen Puffer und 
sich das Hauptprogramm dann selbst die Kopie erstellt?

Der Interrupthandler und die readRB liegen im gleichen Modul (.c-File).

Gruß
Fabian

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Fabian B. schrieb:

> Wenn ich also meinen Ringpuffer nicht mehr volatile qualifiziere, die
> Zeiger (in meinem Fall einfache Indexvariablen) aber weiterhin, und ich
> in meinem Hauptprogramm die "readRB"-Funktion aufrufe (die als extern in
> einem anderen Modul bekannt ist) müsste es reichen oder?

Ja, so sehe ich das.

von (prx) A. K. (prx)


Lesenswert?

Jörg Wunsch schrieb:

> Nein, der Zugriff auf eine Variable macht das nicht,

So dachte ich mir das auch.

> aber ein
> Aufruf einer externen Funktion impliziert die Möglichkeit, dass
> diese Funktion auch globale Daten ändert.

Aber nur wenn der Compiler die Funktion nicht kennt. Man kann aber auch 
alles zusammen übersetzen. In GCC wird das eher selten genutzt, in IBMs 
VisualAge C++ war das jedoch schon vor über 10 Jahren der Regelfall.

Ich vermute, man bewegt sich bei der beschriebenen Flag-Technik in dem 
Bereich "es funktioniert doch", und nicht im streng definierten Bereich. 
Und wäre genau genommen besser dran, mit einer expliziten memory barrier 
im Compiler für klare Verhältnisse zu sorgen. Also sowas wie
   asm volatile ("" : : : "memory");
zwischen dem volatilen Flag-Test und dem nicht volatilen Rest.

Letzthin bin ich dieser Frage im Zusammenhang mit einem RTOS begegnet 
(CooCox), dessen globale Variablen von einer Flag-Variablen geschützt 
werden. Die noch dazu nicht einmal selbst volatile ist - mit Absicht und 
nach reiflicher Überlegung, wie mir der Entwickler auf Anfrage 
mitteilte. Ich fragte mich dabei allerdings, inwieweit es tatsächlich 
helfen würde, nur diese Variable volatile zu machen.

Interessant wäre auch, wie sich das bei Prozessoren verhält, bei denen 
Ladebefehle weit vor dem in C programmierten Zeitpunkt spekulativ im 
realen Registerkontext durchgeführt werden, wie beispielsweise bei IA64.

von Karl H. (kbuchegg)


Lesenswert?

Im Normalfall kann man auch davon ausgehen, dass der Compiler 
Arrayinhalte bzw. Zugriffe in Arrays die nicht über konstante Indizes 
laufen, nicht in Registern vorhalten wird. Soviele Register gibt es gar 
nicht :-) und die laufzeitmässige Analyse ob ein bestimmter Array[Index] 
irgendwo in einem Register schon existiert würde länger dauern, als ganz 
einfach zu laden.

Aber im Grunde stimmt es schon. Das ist alles irgendwo Grauzone. Nach 
dem Buchstaben des Gesetzes müsste man eigentlich alles volatile machen.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

A. K. schrieb:

>> aber ein
>> Aufruf einer externen Funktion impliziert die Möglichkeit, dass
>> diese Funktion auch globale Daten ändert.

> Aber nur wenn der Compiler die Funktion nicht kennt.

Ja.

> Und wäre genau genommen besser dran, mit einer expliziten memory barrier
> im Compiler für klare Verhältnisse zu sorgen.

Ist dummerweise auch eine ziemlich Pessimierung.  Könnte aber
hier passend sein.

> Interessant wäre auch, wie sich das bei Prozessoren verhält, bei denen
> Ladebefehle weit vor dem in C programmierten Zeitpunkt spekulativ im
> realen Registerkontext durchgeführt werden, wie beispielsweise bei IA64.

Man hat bei diesen Prozessoren das Gegenteil von RISC gemacht: man
hat unmassig ,,Intelligenz'' versucht, aus dem Compiler in den Prozessor
zu verlagern.  Dort wäre dann also der Prozessor in der Verantwortung,
die Daten trotzdem erneut aus dem RAM zu laden.  Persönlich finde ich
die RISC-Variante besser, denn erstens kann man dort zumindest im vom
Compiler generierten Code noch nachlesen, was passiert, und zweitens
braucht es sehr viel weniger Strom, als wenn man hunderttausende
Digitalgatter dafür spendiert, Dinge zu implementieren, die ein
Compiler eigentlich genauso gut machen könnte.  Aber das hat uns halt
eine Lobby aus Intel und Microsoft versaubeutelt...  Im realen Kommerz
setzt sich eben selten das durch, was technisch am besten ist.

von (prx) A. K. (prx)


Lesenswert?

Jörg Wunsch schrieb:

> Man hat bei diesen Prozessoren das Gegenteil von RISC gemacht: man
> hat unmassig ,,Intelligenz'' versucht, aus dem Compiler in den Prozessor
> zu verlagern.

Ich bin mir nicht sicher, ob wir hier vom gleichen Prozessor reden. Ich 
denke, du meinst Intels Pentium-4 Varianten, die hierbei weit 
spekulativer arbeiteten als P3/Core2/AMD. Das ist zwar auch ein Thema, 
aber da dieser spekulative Kontext alle naselang über Bord geht hält 
sich das Problem in Grenzen.

Ich sprach von IA64 aka EPIC, bei dem der Compiler Loads explizit weit 
im Voraus startet, auch weit bevor er überhaupt weiss ob er das wirklich 
darf und bevor er weiss ob ihm vielleicht ein Store in die Quere kommt. 
Was der Pentium-4 automatisch im spekulativen Kontext durchführt, tut 
IA64 explizit programmiert im realen Registerkontext. Einem solcherart 
durchgeführten Load kommt nur noch ein Store an die gleiche Adresse in 
die Quere, nichts sonst.

Ein Compiler für IA64 hat also eine starke Motivation, Loads ziemlich 
weit vorzuziehen. Durch ein volatiles Flag geschütze nicht-volatile 
Variablen setzen aber eine bestimmte Reihenfolge der Zugriffe voraus. 
Den volatilen Zugriff kann er nicht vorziehen, die anderen schon - 
Bingo!

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

OK, überzeugt.  Nein, so tief stecke ich bei IA64 dann doch nicht
drin, um das alles zu kennen.

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.