Die folgende Frage ist zugegebenermaßen ein wenig akademisch, praktisch
läßt sich das Problem sehr leicht umgehen, indem man ordentlich
programmiert, also Zuweisung für Zuweisung einzeln aufschreibt.
In C sind ja solche Zuweisungen erlaubt:
a = b = c = bla;
Gestern aben hab ich kurz vorm Sandmännchen sowas probiert:
a = b += c = bla;
Nicht in einem simplen Testprogramm, sondern inmitten eines komplexeren
Programms. Wie fast zu erwarten, kam nicht das erwartete Ergebnis raus:
Ewartet hätte ich, daß a und c zugewiesen werden, und in b aufsummiert
wird.
Leider hab ich momentan keinen Zugang zu einer C Umgebung (bin auf
Arbeit), sonst könnte ich das Verhalten mittels Ausprobieren verstehen.
Muß ich bis heute abend warten. Aber da ich es vor Neugier nicht
aushalte die Frage: was passiert bei so einer Zuweisung? Der Compiler
hat zumindest nicht gemeckert.
Meine Vermutung: die Zuweisung wird von rechts nach links abgearbeitet?
Hatte gedacht jede Variable auf der linken Seite hat ihren eigenen
Zuweisungsoperator direkt zur rechten Seite, aber das scheint nicht so
zu sein?
Und ja, ich weiß: auf diese Art programmieren ist ohnehin kein guter
Stil.
Micha schrieb:> a = b += c = bla;
jeder Operator hat eine Priorität, damit (müsste) sich das so ergeben
a = ( b += (c = bla) )
daraus ergibt sich
a = ( b += bla )
daraus
a = ( b += bla )
(ich hoffe das stimmt so, es wird aber sonst sich jemand finden der den
fehler findet)
Peter II schrieb:> a = ( b += bla )>> daraus>> a = ( b += bla )>> (ich hoffe das stimmt so, es wird aber sonst sich jemand finden der den> fehler findet)
Nur ein Copy-Paste-Fehler. In der letzten Zeile (die zwar richtig ist)
wolltest du wahrscheinlich schreiben:
a = ( b + bla )
Insgesamt kann man die Anweisung:
1
a=b+=c=bla;
folgendermaßen auseinanderpfriemeln:
1
c=bla;
2
b+=c;// d.h. b = bla + c;
3
a=b;// d.h. a = bla + c;
Da die Zuweisungsoperatoren rechtsassoziativ sind, werden die einzelnen
Zuweisungen also einfach der Reihe nach, beginnend bei der rechten,
hingeschrieben.
Micha schrieb:> Leider hab ich momentan keinen Zugang zu einer C Umgebung (bin auf> Arbeit), sonst könnte ich das Verhalten mittels Ausprobieren verstehen.
ideone.com hilft ;-)
Danke für die Antworten auf die praxisbezogen zugegebenermaßen nicht so
wichtige Frage.
Das mit ideone.com find ich toll! Genau so eine Ausprobier-Plattform für
C, zum Testen von Code-Schnipseln, hab ich mir schon immer gewünscht!
Micha schrieb:> Danke für die Antworten auf die praxisbezogen zugegebenermaßen nicht so> wichtige Frage.
Nun, ganz praxisbezogen ist das durchaus nicht ganz unwichtig zu
verstehen, was da in welcher Reihenfolge passiert. Nicht, dass man
im realen Leben ein "+=" in die Mitte reinschreiben würde, aber
sowas auf einem AVR:
1
DDRA=DDRB=DDRC=0xFF;
ist zumindest schon mal nicht wirklich optimal. Der Grund: die
Verkettung (wie oben erläutert) bewirkt, dass DDRB ja nicht direkt
aus 0xFF zugewiesen wird, sondern aus DDRC (und für DDRA dann nochmal
das gleiche). Da die DDRx als Register natürlich "volatile" markiert
sind, zwingt die Verkettung den Compiler, wirklich DDRC erst zurück
zu lesen, bevor der Wert an DDRB zugewiesen wird.
Worst case, man nehme einen ATmega1281 o. ä.:
1
DDRA=DDRB=DDRC=DDRD=DDRE=DDRF=DDRG=0xFF;
Hausaufgabe dazu: welcher Wert steht am Ende in DDRA? ;-)
@Jörg Wunsch (dl8dtl) (Moderator) Benutzerseite
>DDRA = DDRB = DDRC = DDRD = DDRE = DDRF = DDRG = 0xFF;>Hausaufgabe dazu: welcher Wert steht am Ende in DDRA? ;-)
Ich rate mal. Da wahrscheinlich einer der hohen Ports nicht alle Bits
implementiert hat, werden beim Rücklesen irgendwo Nullen reinkommen und
DDRA != 0xFF sein.
Aber wer sowas macht, frißt auch kleine Kinder!
Falk B. schrieb:> Da wahrscheinlich einer der hohen Ports nicht alle Bits implementiert> hat, werden beim Rücklesen irgendwo Nullen reinkommen und DDRA != 0xFF> sein.
Yep, 0x3F oder sowas. :)
Jörg W. schrieb:> Worst case, man nehme einen ATmega1281 o. ä.:> DDRA = DDRB = DDRC = DDRD = DDRE = DDRF = DDRG = 0xFF;>> Hausaufgabe dazu: welcher Wert steht am Ende in DDRA? ;-)
Beim GCC: 0xFF
Peter D. schrieb:> x = UDR0 = 'a';> Was steht nun in x?
Beim GCC: 'a'
Man sollte sich aber nicht darauf verlassen, denn:
1
The implementation is permitted to read the object to determine the
2
value but is not required to, even when the object has volatile-
Yalu X. schrieb:>> Worst case, man nehme einen ATmega1281 o. ä.:>> DDRA = DDRB = DDRC = DDRD = DDRE = DDRF = DDRG = 0xFF;>> Hausaufgabe dazu: welcher Wert steht am Ende in DDRA? ;-)>> Beim GCC: 0xFF
Nö, es war wirklich 0x3F.
@ Hungerkünstler (Gast)
>> Aber wer sowas macht, frißt auch kleine Kinder!>Ich mag Kinder ..... aber ich schaff' kein ganzes.
Versuch's mit nem Kinderdöner! ;-)
Yalu X. schrieb:> Jörg W. schrieb:> Nö, es war wirklich 0x3F.>> Welche Version?
Irgendeine ältere.
> Aus dem aktuellen GCC-Manual:However when assigning to a scalar> volatile, the volatile object is not> reread, regardless of whether the assignment expression's rvalue is used> or not.
Danke für die Ergänzung. Mir war das gar nicht bewusst, dass das
"volatile" hier optional für den Compiler ist.
Jörg W. schrieb:> Danke für die Ergänzung. Mir war das gar nicht bewusst, dass das> "volatile" hier optional für den Compiler ist.
Die oben zitierte Fußnote, die das klar stellt, taucht zum ersten Mal im
Draft N1516 vom 4.10.2010, also relativ spät auf. Vermutlich wurde das
Verhalten des GCC erst daraufhin geändert.
Yalu X. schrieb:> Vermutlich wurde das> Verhalten des GCC erst daraufhin geändert.
Ja, vermutlich. Daher ist für mich eine goldene Regel: "Programmiere
defensiv!". Das heisst: Im Zweifel Ausdrücke vereinfachen und lieber
eine Klammer zuviel als zuwenig. Damit kann man schon viele Fallen
umgehen.
Jörg W. schrieb:> Mir war das gar nicht bewusst, dass das> "volatile" hier optional für den Compiler ist.
Ist aber hier ausnahmsweise mal logisch. Wenn man vorhat, ein flüchtiges
oder unflüchtiges Register mit irgendwas zu BESCHREIBEN, dann ist es
völlig wurscht, was zuvor drinstand. Asterix..Obelix..HauDraufWieNix.
W.S.
W.S. schrieb:> Ist aber hier ausnahmsweise mal logisch. Wenn man vorhat, ein flüchtiges> oder unflüchtiges Register mit irgendwas zu BESCHREIBEN, dann ist es> völlig wurscht, was zuvor drinstand.
Diese Argumentation verstehe ich nicht ganz. Es kam doch vor der oben
zitierten Fußnote doch eher drauf an, was nachher drinstand und nicht
vorher?!?
Also:
PORTA = PORTB = PORTC = 0xFF;
wurde vor der Draft-Änderung interpretiert als:
PORTC = 0xFF;
PORTB = PORTC;
PORTA = PORTB;
Also: Was jeweils genommen wurde, war der Wert nach der vorhergehenden
Zuweisung.
Nach der Änderung von gcc funktioniert das nun so:
PORTC = 0xFF;
PORTB = 0xFF;
PORTA = 0xFF;
... jedenfalls habe ich es so verstanden.
W.S. schrieb:> Ist aber hier ausnahmsweise mal logisch.> ...
Ich empfinde das ebenfalls als logisch, aber aus einem ganz anderen
Grund.
Hier ist der entsprechende Absatz im neueren Draft in vollem Wortlaut:
1
An assignment operator stores a value in the object designated by the
2
left operand. An assignment expression has the value of the left operand
3
after the assignment,¹¹¹⁾ but is not an lvalue. The type of an
4
assignment expression is the type the left operand would have after
5
lvalue conversion. The side effect of updating the stored value of the
6
left operand is sequenced after the value computations of the left and
7
right operands. The evaluations of the operands are unsequenced.
8
9
———————————————————
10
¹¹¹⁾ The implementation is permitted to read the object to determine the
11
value but is not required to, even when the object has volatile-
12
qualified type.
Ohne die Fußnote liest sich der Satz (der von dem C99-Text wörtlich
übernommen wurde)
1
An assignment expression has the value of the left operand after the
2
assignment, […]
tatsächlich so, als müsste zur Ermittlung des Werts des Zuweisungs-
ausdrucks das Volatile-Objekt (d.h. das Register) nach dem Beschreiben
wieder zurückgelesen werden. Wenn man damit aber wirklich Ernst machen
wollte, müsste konsequenterweise das Register aber auch dann gelesen
werden, wenn der Wert gar nicht benutzt wird, denn als R-Value
verwendete Volatile-Objekte müssen immer gelesen werden, auch wenn ihr
Wert sofort wieder verworfen wird, wie bspw. in
1
UDR0;// Ausdruck ohne Zuweisung, UDR0 wird trotzdem gelesen
Somit müsste auch in
1
DDRA=0xFF;
DDRA nach dem Beschreiben sofort wieder zurückgelesen werden, was
natürlich Humbug wäre und von den Normierern sicher auch nicht so
gewollt war.
IMHO wäre in dem Satz
1
An assignment expression has the value of the left operand after the
2
assignment, […]
eindeutiger, wenn man "value of the left operand" durch "value assigned
to the left operand (after type conversion, if required)" ersetzt hätte.
Dann wäre klar, dass das Register nach einer Zuweisung nie zurückgelesen
wird, egal ob der Wert der Zuweisung anschließend noch benutzt wird oder
nicht. Aber mich fragt ja keiner ;-)
In früheren Drafts, wo die Fußnote noch fehlte, war stattdessen der Typ
des Werts der Zuweisung etwas anders spezifiziert:
1
The type of an assignment expression is the type of the left operand
2
unless the left operand has qualified type, in which case it is the
3
unqualified version of the type of the left operand.
Evtl. sollte das Wegfallen der Qualifier (also insbesondere auch von
volatile) ein Hinweis darauf sein, dass das beschriebene Objekt nicht
gelesen werden muss.
Weil das aber vermutlich kaum jemand so verstanden hat, ist die Fußnote
hinzugefügt worden, die in ihrer Formulierung expliziter ist. Und da die
bestehenden Compiler das Zurücklesen unterschiedlich handhabten, wollte
man nicht die eine oder die andere Variante vorschreiben, sondern ließ
explizit beide zu.
Naja, C eben. Dieser Käse mit den abstrusen Mehrfachzuweisungen hat doch
praktisch kaum Sinn. Man muss es ja nicht gleich wie BASCOM machen un
nur einfachste Operationen mit 2 Variablen pro Berechung erlauben, aber
diese C-Stunts sind schlicht der Blinddarm der Programmiersprache. Man
sollte sie nicht (aus)reizen!
Falk B. schrieb:> Naja, C eben. Dieser Käse mit den abstrusen Mehrfachzuweisungen hat doch> praktisch kaum Sinn.
Sobald eine Zuweisung als Teil eines Ausdrucks verwendet werden kann,
wird auch eine Mehrfachzuweisung möglich.
Ohne dies wäre es eine völlig andere Sprache geworden. Incr/Decr-Ops
sind Zuweisungen ziemlich ähnlich, ergeben aber ohne Verwendung in einer
weiteren Rechnung wie c = *++p; wenig Sinn.
Ebenso wird das verwendet in typischen klassischen C Statements wie
while ((c = getchar()) != EOF) ...
Dass auch a = b = c = ... möglich ist, ist nur ein Nebeneffekt.
Petter schrieb:> Toll ist auch sowas:>> a[b] = a[b--];>> Da weiß ich nie, ob zuerst subtrahiert wird und dann links noch das alte> b steht oder nicht...!?
Deshalb bekommst Du auch eine Warnung (irgend was mit sequence-points)
vom Compiler. Ich hab mir angewöhnt die Warneinstellungen stets auf das
striktest mögliche zu stellen und zusätzlich alle Warnungen als Fehler
zu behandeln (bis auf zwei oder drei Ausnahmen) so daß ich gar nicht
erst in Versuchung komme sowas zu schreiben.
Frank Furz schrieb:> Naja, fällt aber eher in die Kategorie "Vergewaltigung einer> for-Schleife".
Ja, da hast du nicht ganz unrecht :)
> Eigentlich schade, dass es mit der normalen Syntax nicht geht.
Der Grund dafür liegt im Wesentlichen darin, dass die For-Schleife eine
kopfgesteuerte Schleife ist. Deren Vorteil liegt darin, dass damit auch
0 Durchläufe möglich sind.
Die Do-While-Schleife ist fußgesteuert, damit sind auch 256 Durchläufe
möglich. Dafür muss man aber auf die Möglichkeit von 0 Durchläufen
verzichten. Auch das von mir gepostete Beispiel mit dem bedingten break
am Ende der For-Schleife ist fußgesteuert.
Ein Schleifentyp, dessen Durchlaufzahl durch einen 8-Bit-Wert definiert
wird und 0 bis einschließlich 256 Durchläufe zulässt, kann es leider
nicht geben.
In Pascal ist die For-Schleife so definiert, dass auch 256 Durchläufe
möglich sind:
1
var
2
i, n: byte;
3
begin
4
read(n);
5
for i:=0 to n do
6
{...}
7
end.
Hier dreht die Schleife (n+1)-mal, für n=255 also 256-mal. 0 Durchläufe
damit sind aber nicht möglich, da dafür n=-1 sein müsste, was außerhalb
des Wertebereichs von byte liegt.
Bei Schleifen mit variabler Durchlaufzahl ist in der Praxis sehr oft die
Möglichkeit von 0 Durchläufen gefordert. Der andere Fall, dass über den
kompletten Wertebereich des Zählerdatentyps iteriert werden soll, kommt
eher seltener vor. Eigens dafür einen fußgesteuerten Zählschleifentyp
vorzusehen, wäre übertrieben.
Deswegen muss man diesen Fall in C eben mit einer Do-While-Schleife
erschlagen (oder für die Zählvariable den nächstgrößeren Datentyp
wählen).
Frank Furz schrieb:> Kann man das hier als for-Schleife schreiben, ohne einen größeren> Datentyp zu verwenden (256 Durchläufe)?>> uint8_t i = 0;> do {> // ...> } while (++i > 0);
@Eric B.:
Deine erste Lösung gefällt mir sehr gut. Der GCC optimiert dabei sogar
das j einschließlich der Zuweisung j=0 weg, so dass am Ende exakt der
gleiche, optimale Code wie bei der Do-While-Schleife herauskommt.
Bei deiner zweiten Lösung macht die Schleife aber doch nur 255
Durchläufe. Damit der Schleifenrumpf trotzdem 256-mal ausgeführt wird,
hast du ihn einfach noch einmal vor die Schleife kopiert, was ja nicht
so besonders elegant ist. Oder habe ich da etwas falsch verstanden?
Yalu X. schrieb:> Bei deiner zweiten Lösung macht die Schleife aber doch nur 255> Durchläufe. Damit der Schleifenrumpf trotzdem 256-mal ausgeführt wird,> hast du ihn einfach noch einmal vor die Schleife kopiert, was ja nicht> so besonders elegant ist. Oder habe ich da etwas falsch verstanden?
Nö, du hast schon richtig verstanden :-)
Es hat den "Vorteil", dass keine extra Variabele benötigt wird. Ich habe
aber keine Ahnung ob irgendein Compiler die Code-Duplizierung
wegoptimieren könnte. Vielleicht wenn der duplizierte Code in einer
inline Funktion gepackt wird.
Yalu X. schrieb:> Deine erste Lösung gefällt mir sehr gut.
Mir nicht. Es sei denn ich wäre Schiedsrichter in einem
Codeobfuscation-Contest. Was spricht degegen einfach den Code
straightforward so hinzuschreiben wie man ihn meint, in dem Fall also
eine simple do/while?
Also, in meinen Augen ist alles, was über die normale Syntax einer
for-Schleife hinausgeht, irgendwie Murks und dann sollte man das Ganze
als while-do oder do-while ausschreiben. Meine Verwirrung stammte
tatsächlich daher, dass ich in meiner Jugend exzessiv Pascal
programmiert habe, wo das Ganze etwas anders ist.
Die Frage ist jetzt: Optimiert es der gcc denn, wenn ich 256 loops
brauche und aus Leichtsinn/Faulheit eine for-Schleife und uint16_t
nehme? Mit einem schlauen Compiler kommt ja vielleicht bei der
for-Variante mit uint16_t der gleiche Code raus wie bei der
do-while-Variante mit uint8_t...
Habe es nicht ausprobiert...
Bernd K. schrieb:> Yalu X. schrieb:>> Deine erste Lösung gefällt mir sehr gut.>> Mir nicht. Es sei denn ich wäre Schiedsrichter in einem> Codeobfuscation-Contest. Was spricht degegen einfach den Code> straightforward so hinzuschreiben wie man ihn meint, in dem Fall also> eine simple do/while?
Du hast den Thread nicht gelesen.
Es ging um diese Frage:
Frank Furz schrieb:> Kann man das hier als for-Schleife schreiben, ohne einen größeren> Datentyp zu verwenden (256 Durchläufe)?>> uint8_t i = 0;> do {> // ...> } while (++i > 0);
Da m.W. im ISO-Normungsgremium keine Bestrebungen bestehen, in C die
Do-While-Schleife abzuschaffen, ist die Diskussion um den besten Ersatz
dafür natürlich rein hypothetischer Natur ;-)
Frank Furz schrieb:> Die Frage ist jetzt: Optimiert es der gcc denn, wenn ich 256 loops> brauche und aus Leichtsinn/Faulheit eine for-Schleife und uint16_t> nehme?
Nein, nicht einmal dann, wenn die Laufvariable im Schleifenrumpf gar
nicht benutzt wird:
1
for(uint16_ti=0;i<256;i++){
2
PORTB=1;
3
}
Das Inkrement wird hier trotzdem als 16-Bit-Operation ausgeführt
(AVR-GCC 6.2.0). Es würde aber nichts gegen so eine Optimierung
sprechen. Vielleicht kommt sie ja in Version 7 :)
Edit:
Auch die Abbruchbedingung i<=255 statt (i<256) ändert nichts daran.
Frank Furz schrieb:> Die Frage ist jetzt: Optimiert es der gcc denn, wenn ich 256 loops> brauche und aus Leichtsinn/Faulheit eine for-Schleife und uint16_t> nehme?
Man nehme folgenden Test:
1
#include<stdint.h>
2
3
externuint8_t*data;
4
5
uint8_t
6
sum_data(void)
7
{
8
uint8_tresult=0;
9
#ifdef DOWHILE
10
uint8_ti=0;
11
do{
12
result+=data[i];
13
}while(++i!=0);
14
#else
15
for(inti=0;i<256;i++)
16
result+=data[i];
17
#endif
18
returnresult;
19
}
Erste Version, mit -DDOWHILE compiliert:
1
sum_data:
2
/* prologue: function */
3
/* frame size = 0 */
4
/* stack size = 0 */
5
.L__stack_usage = 0
6
lds r20,data
7
lds r21,data+1
8
ldi r25,0
9
ldi r24,0
10
.L2:
11
movw r30,r20
12
add r30,r25
13
adc r31,__zero_reg__
14
ld r18,Z
15
add r24,r18
16
subi r25,lo8(-(1))
17
brne .L2
18
/* epilogue start */
19
ret
Zweite Version, ohne DOWHILE:
1
sum_data:
2
/* prologue: function */
3
/* frame size = 0 */
4
/* stack size = 0 */
5
.L__stack_usage = 0
6
lds r30,data
7
lds r31,data+1
8
movw r18,r30
9
inc r19
10
ldi r24,0
11
.L2:
12
ld r25,Z+
13
add r24,r25
14
cp r30,r18
15
cpc r31,r19
16
brne .L2
17
/* epilogue start */
18
ret
Witzigerweise also komplett anders implementiert, die Variable i
gibt es gar nicht mehr, sondern das Schleifenende wird als Vergleich
des aktuellen Zeigers gegen einen Zielwert realisiert, wobei der
GCC (5.3.0) beim Berechnen des Zielwertes gut geschnallt hat, dass
er für die +256 lediglich das höhere Byte des Zählers inkrementieren
muss.