Forum: Compiler & IDEs GCC - seltsames Verhalten bei for-Schleife


von Thomas (Gast)


Lesenswert?

Tach auch,

ich habe mich jetzt auch endlich mal der Mikrocontrollerprogrammierung 
gewidmet und ein erstes Progrämmchen in C verfasst:
1
// Mikrocontroller Hello World
2
// Blinkende LED
3
// Blinkintervall sinkt stetig um DELTA_T von T_START bis T_STOP.
4
// Danach gehts von vorne los.
5
6
#define T_START 0xFFFF
7
#define T_STOP  0x00FF
8
#define DELTA_T 0x00FF
9
10
#include <avr/io.h>
11
12
void init(void);
13
14
int main(void){
15
16
  init();
17
18
  uint16_t i,n;
19
20
  while(1){
21
    for(i=T_START; i>T_STOP ; i=i-DELTA_T){  // Blinkfrequenz steigt stetig
22
      for(n=i; n>0; n--);    // Rechenzeit verballern
23
      PORTA = PORTA ^ 0x01;  // LED an Pin A0 togglen
24
    }
25
  }
26
27
  return 0;
28
}
29
30
void init(void){
31
  DDRA  = 0xff;  // Port A als Ausgang
32
  PORTA = 0xff;  // Alle LEDs aus
33
}

Funktioniert auch prima. Allerdings stellt mich das Verhalten der ersten 
for-Schleife vor ein Rätsel. Ich hatte mir überlegt, dass diese Schleife 
ja eigentlich niemals verlassen werden muss, so dass ich mir die 
while(true)-Schleife sparen kann. Also habe ich die Ausführbedingung 
"true" gesetzt. Da "i" unsignet ist sollte das ja eigentlich 
funktionieren. Aber Pustekuchen, der durchläuft die Schleife einmal, 
schaltet die LED ein und dann ist Feierabend. Und das obwohl die 
while(1)-Schleife noch da war...
OK, anderer Versuch: Ausführbedingung auf >=0 gesetzt -> gleiches 
Verhalten. Selbst >0 führt zum Absturz, und das obwohl es bei den 
eingestellten Werten für T_START und DELTA_T eine Punktlandung auf 0 
gibt, eine Abbruchbedingung also klar definiert ist!
Setzt man aber z.B. T_STOP auf 1 und DELTA_T auf 0x00FE (wegen der 
Punktlandung) stellt sich das gewünschte Verhalten ein.

Folgende Version führt übrigens zu dem gleichen Fehler:
1
...
2
uint16_t i,n;
3
i = T_START;
4
while(1){
5
  i = i - DELTA_T;  // Blinkfrequenz steigt stetig
6
  for(n=i; n>0; n--);  // Rechenzeit verbraten
7
  PORTA = PORTA ^ 0x01;  // LED an Port A0 togglen
8
}
9
...

Ich benutze übrigens die aktuelle Version von WinAVR.


Leicht verwirrte Grüße,
Thomas

von Andreas K. (a-k)


Lesenswert?

Rechenzeit verballern klappt so nicht. Scheinbar sinnlosen Code erkennt 
der Compiler und wirft ihn raus. Delay-Funktion verwenden.

von karl (Gast)


Lesenswert?

...oder einfach in deine Schleife z.B. ein Port Hin und Her schalten 
lassen -  z.B. einen der Portbits, die nicht nach außen geführt sind  - 
je nach Ausführung ...

von gast (Gast)


Lesenswert?

asm volatile ( "nop" ); in die for schleife packen :)

von Thomas (Gast)


Lesenswert?

Tatsächlich,

so geht`s immer:
1
for(n=i; n>0; n--) asm volatile("nop");  // Rechenzeit verballern
Bleibt die Frage, warum es in manchen Situationen auch ohne "nop" geht. 
Da schlägt wahrscheinlich die Optimierung zu...

von Rolf Magnus (Gast)


Lesenswert?

Was ist eigentlich so schlimm an den delay-Funtkionen, daß jeder hier 
auf Teufel-komm-raus nach anderen Möglichkeiten sucht, Rechenzeit zu 
verbraten?

von Andreas K. (a-k)


Lesenswert?

Ist das "not invented here" Syndrom.

von Thomas (Gast)


Lesenswert?

Wie bereits gesagt ist das mein erstes uC-Programm und man kann 
schließlich nur die Funktionen anwenden, die man auch kennt. Und durch 
Spielen lernt man schließlich immer noch am besten, hier z.B. die Tücken 
des Compilers.
Abgesehen davon machen die delay-Funktionen, die man über "util/delay.h" 
erreicht auch nichts anderes als Rechenzeit zu verbraten...
Elegant macht man sowas natürlich über Timerinterrupt, das ist mir auch 
klar.

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


Lesenswert?

Thomas wrote:

> Abgesehen davon machen die delay-Funktionen, die man über "util/delay.h"
> erreicht auch nichts anderes als Rechenzeit zu verbraten...

Dafür tun sie das aber im Gegensatz zur
"for(i = 0; i < 30000; i++) /* wait */;"-Schleife auch wirklich. ;-)

von Thomas (Gast)


Lesenswert?

Das Problem ist wohl, dass ich einfach noch nicht so denke wie ein 
Compiler. Ich habe das gleiche Programm kurz vorher auch in Assembler 
implementiert, von dieser hardwarenahen Betrachtung muss man sich wohl 
in C ein Stück weit lösen. Aber deswegen experimentier ich ja auch und 
finde es gut, wenn ich über solche Probleme stolper. Als nächstes währe 
es vielleicht mal interessant das C-HEX zu disassemblieren und mit 
meinem Ergebnis zu vergleichen...

von Thomas (Gast)


Lesenswert?

Ach ja, und die AVR-libc Lösung wird doch wohl im Kern auch nicht ganz 
anders implementiert sein, als "for(i = 0; i < 30000; i++) asm 
volatile("nop");", oder?

von Gast (Gast)


Lesenswert?


von Peter D. (peda)


Lesenswert?

Thomas wrote:
> Das Problem ist wohl, dass ich einfach noch nicht so denke wie ein
> Compiler. Ich habe das gleiche Programm kurz vorher auch in Assembler
> implementiert, von dieser hardwarenahen Betrachtung muss man sich wohl
> in C ein Stück weit lösen.


Ja, man muß völlig umdenken, wenn man in C programmiert. C hat keinerlei 
Zeitbezug.
Es kümmert sich nicht, wann etwas gemacht wird, wie lange etwas dauert 
oder in welcher Reihenfolge etwas gemacht wird.
Für C ist eine Rechnung immer dann richtig, wenn am Ende das Ergebnis 
stimmt.

Spezielle Compiler für Microcontroller sind in der Regel näher an der 
Hardware, d.h. sie sortieren Operationen nicht um, wenn sich 
Ausführungszeit oder Codebedarf dadurch nicht verringern.

Der GCC gehört leider nicht dazu. Der GCC hat den Hang, Sachen möglichst 
spät auszuführen (ist quasi faul), auch wenn dadurch der Code- und 
Zeitbedarf oftmals steigt.

Für den GCC sind nur volatile Zugriffe Fixpunkte, d.h. wenn man z.B. 
einen (volatile definierten) Portpin setzt, müssen alle dafür 
erforderlichen Berechnungen ausgeführt worden sein.

Hört sich vielleicht schlimmer an, als es ist, aber man muß es im 
Hinterkopf behalten, daß für den GCC Zeit und Abläufe keine Rolle 
spielen.
Benötigt man das, muß man es durch volatile Zugriffe erzwingen.

Wenn Du Assembler kannst, solltest Du ruhig mal ins Listing schauen, Du 
wirst staunen, was der Compiler da alles umsortiert.
Z.B. kurze Funktionen stehen zwar im Listing, werden aber nie 
aufgerufen, weil sie im Aufrufer nochmal dupliziert (geinlined) sind.


Peter

von Rolf Magnus (Gast)


Lesenswert?

> so geht`s immer:
> for(n=i; n>0; n--) asm volatile("nop");  // Rechenzeit verballern
>
> Bleibt die Frage, warum es in manchen Situationen auch ohne "nop" geht.

Ohne das nop geht's durchaus auch. Es reicht ein:
1
for(n=i; n>0; n--) asm volatile("");  // Rechenzeit verballern

Mir ist klar, daß du das nicht so gemeint hast. Ich wollte es aber nur 
der Vollständigkeit halber erwähnen.

> Das Problem ist wohl, dass ich einfach noch nicht so denke wie ein
> Compiler.

Nun, der Compiler hat einen Optimizer, und dessen Zweck ist es, das, was 
du in deinem Code formulierst, so umzubauen, daß es in möglichst kurzer 
Zeit und/oder mit möglichst wenig Code ausgeführt wird. Und wenn er 
merkt, daß ein Teil deines Codes absolut gar nichts tut, wird er diesen 
durch absolut gar nichts ersetzen, da das immer noch am wenigsten 
Rechenzeit und Codespeicher benötigt. Damit ist eine leere Schleife 
natürlich ein gefundenes Fressen für den Optimizer. Er weiß ja nicht, 
daß du in diesem speziellen Fall eben nicht möglichst effizienten Code 
willst.

> Ich habe das gleiche Programm kurz vorher auch in Assembler
> implementiert, von dieser hardwarenahen Betrachtung muss man sich wohl
> in C ein Stück weit lösen.

Ja. C wird zwar oft als portabler Assembler bezeichnet, aber das stimmt 
eben so nicht.

> Als nächstes währe es vielleicht mal interessant das C-HEX zu
> disassemblieren und mit meinem Ergebnis zu vergleichen...

Das ist eine gute Idee.

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


Lesenswert?

Peter Dannegger wrote:

> Der GCC gehört leider nicht dazu. Der GCC hat den Hang, Sachen
> möglichst spät auszuführen (ist quasi faul), auch wenn dadurch der
> Code- und Zeitbedarf oftmals steigt.

Peter, auch wenn du diese Behauptung gebetsmühlenartig wiederholst,
bleibt sie nichts anderes als FUD.

Selbstverständlich ist es das erklärte Ziel des Optimierers, den
Codegrößen- bzw. Ausführungszeitbedarf des erzeugten Codes zu
reduzieren.  Dass er dieses Ziel beim AVR-GCC nicht immer erreicht,
liegt vornehmlich daran, dass den GCC-Entwicklern kein automatisiertes
Werkzeug zur Verfügung steht, mit dem sie eine ggf. entstehende
Verschlechterung für das Target "avr" bereits während ihrer
Entwicklungsarbeit feststellen könnten.  Daher kommt es also zu einem
neuen Compiler-Release, und die AVR-Nutzer können dann erst nachher
feststellen, ob irgendwas besser oder schlechter geworden ist.

Wenn du statt der Verbreitung von FUD zu fröhnen lieber aktiv
mithelfen willst, dass ungünstige Optimierungen repariert werden, dann
schreib bitte für alles, was du findest, ordentliche Bugreports (in
GCCs Bugzilla, nicht einfach irgendwo in ein Forum) und diskutiere das
dann mit den GCC-Entwicklern durch.  Wenn du zeigen kannst dass
zwischen zwei Compilerversionen etwas schlechter geworden ist, dann
kannst du das im Subject des Bugreports mit [regression] markieren,
damit bekommt es in der Bearbeitung eine gewisse Priorität.

Es hilft hier niemandem was, wenn du stets aufs neue lamentierst, dass
die Compiler der vorvorherigen Generation, die in der Tat noch
,,bessere Assembler'' waren, ja ach so viel besser waren.  (Das waren
sie nämlich gar nicht, bei denen musstest du einfach als Programmierer
noch die Hälfte der Optimierung übernehmen.  Ja, dein Code macht das
auch, aber sorry, dafür hat er leider nicht den Ruf, dass man ihn
sofort beim Angucken versteht.)

> Für den GCC sind nur volatile Zugriffe Fixpunkte, d.h. wenn man z.B.
> einen (volatile definierten) Portpin setzt, müssen alle dafür
> erforderlichen Berechnungen ausgeführt worden sein.

Naja, und wie wir neulich festgestellt haben, fehlt es eigentlich im
C-Standard teilweise auch an Konzepten, die dem besser gewordenen
Optimierungspotenzial der Compiler dahingehend Rechnung tragen, dass
man als Programmierer ggf. Prioritäten setzen kann.  Stichwort
`volatile code block' oder sowas.  Das kannst du aber nicht dem
Compiler anlasten, sondern wenn du das geändert haben willst, musst du
das irgendwie ins Standard-Kommittee reinbringen.  Ein möglicher Weg,
wie das passieren kann ist, dass du mit den GCC-Entwicklern dafür
einen Weg diskutierst und versuchst, dass es zu einer
Beispielimplementierung kommt.  Eine funktionierende
Beispielimplementierung stellt für das Standardkommittee eine gute
Basis dar, bestimmte Dinge ggf. mal offiziell zu übernehmen.  Das war
auch der Grund, warum ich beispielsweise die 0b-Binärkonstanten im GCC
versucht habe durchzuziehen (was mir mittlerweile ja auch gelungen
ist).

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


Lesenswert?

Thomas wrote:

> Ach ja, und die AVR-libc Lösung wird doch wohl im Kern auch nicht ganz
> anders implementiert sein, als "for(i = 0; i < 30000; i++) asm
> volatile("nop");", oder?

Sie macht es als inline-Assembler-Schleife und ist damit (im Inneren,
also _delay_loop_1() und _delay_loop_2()) unabhängig von den
Optimierungseinstellungen des Compilers.

von Andreas Paulin (Gast)


Lesenswert?

Ohooo.....unabhängig von den Optimierungseinstellungen?
Da hab ich aber was ganz anderes gehört:

Beitrag "delay bringt mich zum verzweifeln; Optimization level?"

von Rolf Magnus (Gast)


Lesenswert?

> Ohooo.....unabhängig von den Optimierungseinstellungen?

Ja.

> Da hab ich aber was ganz anderes gehört:

Das ist _delay_ms, nicht _delay_loop_1 und _delay_loop_2. Bei _delay_ms 
kommt eine Fließkomma-Berechnung dazu, bei der es praktisch 
lebensnotwendig ist, daß sie zur Compilezeit ausgeführt wird.

von Andreas Paulin (Gast)


Lesenswert?

OK. Passt.
Gibts denn beim GCC nicht die Möglichkeit, die Optimierung für einzelne 
Codesegmente unterschiedlich einzustellen?
So a la
#pragma O1
?

von Uhu U. (uhu)


Lesenswert?

@ Peter Dannegger:

> Ja, man muß völlig umdenken, wenn man in C programmiert. C hat
> keinerlei Zeitbezug.
> Es kümmert sich nicht, wann etwas gemacht wird, wie lange etwas
> dauert oder in welcher Reihenfolge etwas gemacht wird.
> Für C ist eine Rechnung immer dann richtig, wenn am Ende das
> Ergebnis stimmt.

Das sehe ich nun garnicht so: In meinen Augen ist C ein 
Highlevel-Assembler. Eine maschinennähere höhere Programmiersprache, als 
C, ist mir bisher nicht begegnet. Wer jemals in einer Sprache wie C++ 
(unter Ausnutzung der abstrakten Sprachfeatures und z.B. STL), Pascal, 
Ada progammiert hat, weiß was ich meine.

Im übrigen ist einem Assembler das Zeitverhalten des Codes genauso 
wurscht, wie jedem Hochsprachen-Compiler, incl. C: Dafür ist 
ausschließlich der Programmierer verantwortlich.

Wenn der nicht weiß, was er tut, dann ist er ein Stümper...

von Rolf Magnus (Gast)


Lesenswert?

> Das sehe ich nun garnicht so: In meinen Augen ist C ein
> Highlevel-Assembler.

Die C-Norm definiert das aber bewußt anders. Dort gibt es die "as-if 
rule". Die besagt, daß nicht genau das getan werden muß, was in der Norm 
steht, sondern nur etwas, dessen Effekt genau so ist, als ob das getan 
wurde, was dort steht, und die Ausführungszeit gehört hier nicht dazu.

> Im übrigen ist einem Assembler das Zeitverhalten des Codes genauso
> wurscht, wie jedem Hochsprachen-Compiler, incl. C: Dafür ist
> ausschließlich der Programmierer verantwortlich.

In Assembler ja. In C ist der Optimizer auch mit dafür verantwortlich. 
In einer Hochsprache werden viele Elemente der Zielplattform 
wegabstrahiert, und das gilt auch für C. Es sind zwar keine 
Objektorientierung oder Exceptions in die Sprache eingebaut, aber es 
gibt viele andere Abstraktionen, z.B. existieren in C die Register 
nicht, und genausowenig gibt es die Flags oder Interrupts. Sobald du von 
der Hardware abstrahierst, mußt du die Sachen anders schreiben, als man 
es direkt für die Hardware müßte. Auf einmal ist eben der Compiler dafür 
verantwortlich, die Register effizient zu nutzen. Der Programmierer 
definiert nur noch Variablen. Schon hier muß vom Compiler Optimierung 
betrieben werden, die der Programmierer in C einfach nicht mehr machen 
kann, weil er nicht mehr an die Register kommt, und er soll sich da auch 
gar nicht drum kümmern müssen.
Die Idee ist dabei nicht nur, von der Hardware unabhängiger zu werden, 
sondern auch, daß man Dinge lesbarer und einfacher formulieren kann. 
Abstraktion ist aber fast immer mit einem gewissen Overhead verbunden, 
und um den möglichst gering zu halten, dazu besitzt der Compiler den 
Optimizer. Das erleichtert es, Programme so zu schreiben, daß sie 
leichter verständlich sind. Die Implementation wird dadurch oft weniger 
effizient, aber das soll dann der Optimizer automatisch ausgleichen.
In C kann man trotzdem noch sehr hardwarenah programmieren, aber wenn 
man irgendwo ein ganz bestimmtes Timingverhalten braucht, ist C sowieso 
schon zu abstrakt.

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.