Forum: Mikrocontroller und Digitale Elektronik AVR-GCC: Code wird totoptimiert


von Karl K. (karl_k69)


Lesenswert?

Hi,

folgendes Codefragment optimiert mir der AVR-GCC kaputt:

    static unsigned int pulseOffCtr=0;

    main ()
    {
       ...
       while (((volatile unsigned int)pulseOffCtr)>0)
       {
           if (((volatile unsigned int)pulseOffCtr)>6)
           usbPoll();
       }
       ...
    }

pulseOffCtr wird in einer Timer-getriggerten ISR verändert. Aus 
Geschwindigkeitsgründen ist die Variable selber NICHT als volatile 
definiert. Der Compiler hält den Code in der while-Schleife jetzt für 
Unfug und rationalisiert diesen weg. Spannenderweise hilft es auch 
nicht, vor Ort auf volatile zu casten (so wie im Codebeispiel oben).

Wie kann ich den GCC davon abhalten, den Codeteil kaputtzuoptimieren? 
pulseOffCtr generell als volatile zu definieren oder den 
Optimierungslevel runterzuschalten kommt leider nicht in Frage.

von Matthias L. (Gast)


Lesenswert?

>pulseOffCtr generell als volatile zu definieren

Dann lebe damit.

von PittyJ (Gast)


Lesenswert?

> pulseOffCtr generell als volatile zu definieren
Warum?
Warum nicht gleich richtig machen?

von W.A. (Gast)


Lesenswert?

Karl K. schrieb:
> Wie kann ich den GCC davon abhalten, den Codeteil kaputtzuoptimieren?

Inline Assembler?

von Karl K. (karl_k69)


Lesenswert?

W.A. schrieb:
> Inline Assembler?

Coole Idee - Danke!

von Falk B. (falk)


Lesenswert?

@ Karl K. (karl_k69)

>folgendes Codefragment optimiert mir der AVR-GCC kaputt:


>       while (((volatile unsigned int)pulseOffCtr)>0)

Solche Cast sind immer ein klares Zeichen von Murks.

>pulseOffCtr wird in einer Timer-getriggerten ISR verändert. Aus
>Geschwindigkeitsgründen ist die Variable selber NICHT als volatile
>definiert.

Das ist ein Oxymoron. Mach die Variable volatile und fertig. Es wird 
nicht nennenswert langsamer, denn deine ISR braucht so oder so einige 
Takte.

> Der Compiler hält den Code in der while-Schleife jetzt für
>Unfug und rationalisiert diesen weg.

Recht hat er.

>pulseOffCtr generell als volatile zu definieren oder den

Ja.

von Stefan E. (sternst)


Lesenswert?

Falk B. schrieb:
> Mach die Variable volatile und fertig. Es wird
> nicht nennenswert langsamer, denn deine ISR braucht so oder so einige
> Takte.

Außerdem kann man sich auch in der ISR auf einfache Weise mit Hilfe 
einer lokalen Variable des volatile entledigen.

von Kaj (Gast)


Lesenswert?

Karl K. schrieb:
> Aus Geschwindigkeitsgründen
Sinnlose Mikrooptimierung

Karl K. schrieb:
> folgendes Codefragment optimiert mir der AVR-GCC kaputt:
Wenn du kaputten Code schreibst, brauchst du dich nicht beschweren, wenn 
das, was dabei rauskommt, nicht funktioniert. Den GCC trifft da keine 
Schuld.

Wenn du eine Funktion (den GCC) mit falschen Daten (dein Code) fütterst, 
brauchst du dich nicht über ein falsches Ergebnis (Code, der nicht so 
funktioniert wie du das gerne hättest) wundern.

Der Compiler weiß nicht was du dir denkst, der Compiler wertet aus, 
was du da als Code hinschreibst. Wenn der geschriebene Code nicht das 
ist, was du denkst, warum sollte der Compiler schuld sein?

von (prx) A. K. (prx)


Lesenswert?

Karl K. schrieb:
> Spannenderweise hilft es auch
> nicht, vor Ort auf volatile zu casten (so wie im Codebeispiel oben).

Nicht weiter erstaunlich. Du castest auf diese Art nicht die Variable 
selbst, sondern den gelesenen Wert. Das ist zu spät. Allenfalls
  *(volatile ... *)&variable
brächte dich weiter. Nur wirst du dann möglicherweise feststellen, dass 
du das an diversen anderen Stellen auch machen musst und dich jenem Code 
näherst, denn du auch mit "volatile" erhältst.

> Wie kann ich den GCC davon abhalten, den Codeteil kaputtzuoptimieren?

GCC passend für deine Interpretation von C umschreiben.

: Bearbeitet durch User
von Dr. Sommer (Gast)


Lesenswert?

Karl K. schrieb:
>     static unsigned int pulseOffCtr=0;
>
>     main ()
>     {
>        ...
>        while (((volatile unsigned int)pulseOffCtr)>0)
>        {
>            if (((volatile unsigned int)pulseOffCtr)>6)
>            usbPoll();
>        }
>        ...
>     }
Du sollst auch nicht das Ergebnis der Variable nach dem Auslesen 
casten, sondern die Variable selbt nach volatile casten:
1
while (*((volatile unsigned int*) &pulseOffCtr) > 0) { ... }

Falls du C++ verwendest gehts etwas schöner:
1
while (const_cast<volatile unsigned int&> (pulseOffCtr) > 0) { ... }

Beim Verwenden von (unsigned) int auf 8-Bit-AVR ist natürlich besondere 
Vorsicht geboten, da Zugriffe darauf nicht atomar sind. Du musst vor dem 
Zugriff die Interrupts sperren.

von Stefan F. (Gast)


Lesenswert?

Was Dir eventuell hilft, ist ein Hybrid aus beiden. Du deklarierst die 
Variable als volarile, verwendest aber für if() einen 
zwischengespeicherten Wert:
1
static volatine unsigned int pulseCtr=0;
2
3
main()
4
{
5
       unsigned int tmp;
6
       while (tmp=pulseCtr)
7
       {
8
           if (tmp>6)
9
           usbPoll();
10
       }
11
}

So wird die "langsame" Variable nur einmal pro Schleifendurchlauf 
gelesen. Bei der if-Abfrage wird der Wert verwendet, der im Kopf der 
Schleife gelesen wurde. Der Compiler wird tmp in ein Register speichern, 
was schneller erreichbar ist, als eine Variable im RAM. Der 
Kopiervorgang im Schleifenkopf ist zwangsläufg ohnehin nötig.

Hier ist noch ein kleiner Fehler: Die Variable ist größer als 8 Bit, du 
solltest den Lesezugriff daher Atomar machen. Sonst kann es passieren, 
dass der Counter während des Lesens verändert wird, und das führt je 
nach Wert zu völlig falscher Funktion.

Wenn zum Beispiel der Wert mittem beim Lesezugriff von 255 nach 256 
wechselt, bekommst du (tmp) unerwartet den Wert 511.

Den fehler hatte ich selbst neulich gemacht und wurde von anderen 
Forenmitgliedern korrigiert. Nun gebe ich das gelernte gerne weiter :-)

Korrekturvorschlag:
1
static volatine unsigned int pulseCtr=0;
2
3
unsigned int getPulseCounter()
4
{
5
    cli();
6
    unsinged int copy=pulseCtr;
7
    sei();
8
    return copy;
9
}
10
11
main()
12
{
13
       unsigned int tmp;
14
       while (tmp=getPulseCounter())
15
       {
16
           if (tmp>6)
17
           usbPoll();
18
       }
19
}

In diesem Zusammenhang könnte es interessant sein, sich in das Thema 
"inline" einzulesen.

von (prx) A. K. (prx)


Lesenswert?

Übrigens kann der Code auch mit "volatile" immer noch ab und zu in die 
Hose gehen, sobald diese 2-Byte-Variable Werte ausserhalb 0..255 
annimmt. Weil der Zugriff darauf nicht atomar ist. Siehe
https://www.mikrocontroller.net/articles/Interrupt#Atomarer_Datenzugriff

von Tse (Gast)


Lesenswert?

Karl K. schrieb:
> pulseOffCtr wird in einer Timer-getriggerten ISR verändert. Aus
> Geschwindigkeitsgründen ist die Variable selber NICHT als volatile
> definiert.
Schau mal in generierten ASM Code nach, ob die ISR einfach nur schneller 
wird weil der Compiler pulseOffCtr einfach rausschmeißt...

Zeig* uns mal deine ISR, vielleicht ist sie ja nur ungeschickt 
geschrieben.

*Mit C-Formatierung
1
[c] C-Code [/c]

von Peter D. (peda)


Lesenswert?

Karl K. schrieb:
> pulseOffCtr wird in einer Timer-getriggerten ISR verändert.

Dann mußt Du den Zugriff atomar kapseln (int = 2 Byte).

von Bastler (Gast)


Lesenswert?

Volatile bleibt volatile.
Wenn ich in einer ISR verhindern will, daß volatile den GCC am 
optimieren hindert, dann Kopier ich mir die Variable in eine lokale 
Variable und am Ende wieder zurück. GCC hält die in der Regel in einem 
Register und erzeugt damit sehr kompakten Code. Wichtig ist nur: keine 
andere ISR darf auch an der Variable herumspielen. Und die Hauptschleife 
sieht die Variable, wie sie ist: ohne erkennbaren Zusammenhang zur Zeit.

von Falk B. (falk)


Lesenswert?

@Dr. Sommer (Gast)

>Du sollst auch nicht das Ergebnis der Variable nach dem Auslesen
>casten, sondern die Variable selbt nach volatile casten:

>while (*((volatile unsigned int*) &pulseOffCtr) > 0) { ... }

Und wo ist dann der Vorteil zu einer einfachen volatile Deklaration?

Wie man Probleme mit Casts löst, die man ohne sie nie hätte . . .

von Peter II (Gast)


Lesenswert?

Falk B. schrieb:
> Und wo ist dann der Vorteil zu einer einfachen volatile Deklaration?

das sehen wir leider nicht, weil er die ISR nicht gepostet hat. Diese 
wird ohne das volatile zumindest schneller.

Ich mag diese globale volatile auch nicht, weil es viel zu viel von der 
vorhanden Optimierung verhindert.

von Tom (Gast)


Lesenswert?

Peter II schrieb:
> Diese
> wird ohne das volatile zumindest schneller.

Da diese nicht unterbrochen werden kann, hilft der Trick mit der lokalen 
Variablen, wenn viel mit der globalen Variablen gerechnet wird.

von Peter II (Gast)


Lesenswert?

Tom schrieb:
> Peter II schrieb:
>> Diese
>> wird ohne das volatile zumindest schneller.
>
> Da diese nicht unterbrochen werden kann, hilft der Trick mit der lokalen
> Variablen, wenn viel mit der globalen Variablen gerechnet wird.

ist Geschmacksache, 2 Variabel mit dem gleichen Inhalt die man nicht 
vergessen darf zurückzuschreiben (mal schnell ein return reingeschrieben 
und auf einmal geht nichts mehr) oder das hässliche cast.

von Bastler (Gast)


Lesenswert?

Der Cast macht es auf alle Fälle leichter, sich in dessen Funktion zu 
verschätzen.
Wenn die ISR so unübersichtlich wird, daß man eingestreute return's 
nicht erkennt, ...
Zudem gibt es ja auch die Möglichkeit, daß man die globale Variable gar 
nicht immer verändern will.

von Henry P. (henrylexx)


Lesenswert?

Bastler schrieb:
> Volatile bleibt volatile.
> Wenn ich in einer ISR verhindern will, daß volatile den GCC am
> optimieren hindert, dann Kopier ich mir die Variable in eine lokale
> Variable und am Ende wieder zurück. GCC hält die in der Regel in einem
> Register und erzeugt damit sehr kompakten Code. Wichtig ist nur: keine
> andere ISR darf auch an der Variable herumspielen. Und die Hauptschleife
> sieht die Variable, wie sie ist: ohne erkennbaren Zusammenhang zur Zeit.

Querleser: mir ist die Option eine Variable "automar" zusetzen noch nie 
untergekommen, um das Umzusetzen kann man sich hier an die Möglichkeit 
von Stefan U. halten?

Stefan U. schrieb:
> Korrekturvorschlag:
> static volatine unsigned int pulseCtr=0;
>
> unsigned int getPulseCounter()
> {
>     cli();
>     unsinged int copy=pulseCtr;
>     sei();
>     return copy;
> }
>
> main()
> {
>        unsigned int tmp;
>        while (tmp=getPulseCounter())
>        {
>            if (tmp>6)
>            usbPoll();
>        }
> }

?

von Markus F. (mfro)


Lesenswert?

Stefan U. schrieb:
> unsinged int copy=pulseCtr;

sehr unmusikalischer Code ;)

von dunno.. (Gast)


Lesenswert?

Henry P. schrieb:
> Querleser: mir ist die Option eine Variable "automar" zusetzen noch nie
> untergekommen, um das Umzusetzen kann man sich hier an die Möglichkeit
> von Stefan U. halten?

Atomarer Zugriff: Wenn meine Anweisung für einen (kritischen/parallelen) 
Variablenzugriff in mehr als einen ASM-Befehl übersetzt wird, muss ich 
diesen Zugriff gegen Interrupts schützen, also unteilbar / atomar machen 
- Richtig.

Bei AVRs mit
1
cli(); // Interrupts sperren
2
DoSomethingDangerous();
3
sei; // Interrupts freigeben

von Peter D. (peda)


Lesenswert?

Besser so:
1
#include <util/atomic.h>
2
3
  ATOMIC_BLOCK(ATOMIC_FORCEON){
4
    return pulseCtr;
5
  }

von Ralf G. (ralg)


Lesenswert?

Henry P. schrieb:
> Querleser: mir ist die Option eine Variable "automar" zusetzen noch nie
> untergekommen, um das Umzusetzen kann man sich hier an die Möglichkeit
> von Stefan U. halten?

Das ist so nicht ganz in Ordnung. Wenn man so eine Funktion ...
Stefan U. schrieb:
> unsigned int getPulseCounter()
..., z.B., in einer ISR aufrufen würde, hätte man danach die Interrupts 
eingeschaltet.

Die korrekte Herangehensweise ist:
- den Interruptstatus merken
- Interrupts abschalten
- de vorherigen Status wieder herstellen

Dafür gibt's (mit etwas Schreibarbeit) Makros in der <util/atomic.h>.

von Stefan F. (Gast)


Lesenswert?

> Besser so:

Wieso besser? Das erzeugt exakt den gleichen Rahmen mit cli() und sei().

von Stefan F. (Gast)


Lesenswert?

> in einer ISR aufrufen würde, hätte man danach die
> Interrupts eingeschaltet.

So war das ja nicht gedacht. In der ISR soll man natürlich direkt auf 
die Variable zugreifen.

von Peter D. (peda)


Lesenswert?

Ralf G. schrieb:
> - den Interruptstatus merken

In der Regel weiß man ja, daß man gerade einen Interrupthandler schreibt 
und kann direkt auf die Variable zugreifen, da der AVR ohne dirty Tricks 
keine nested Interrupts kann.

Ich hab das ATOMIC_RESTORESTATE aber einmal gebraucht, da Code erst im 
Init vor der Interruptfreigabe und dann im Main aufgerufen wurde.

von Ralf G. (ralg)


Lesenswert?

Ralf G. schrieb:
> ... würde, hätte ...

Stefan U. schrieb:
> ... natürlich ...

Es steht eben an der Funktion nicht dran, das die für ISRs strengstens 
verboten ist! (Abgesehen davon, dass Funktionsaufrufe in einer ISR 
eher 'nicht so optimal' sind.)

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Stefan U. schrieb:
> Wieso besser?

Es erschlägt das volatile gleich mit.
Es läßt sich leichter auf andere CPUs portieren.

von Ralf G. (ralg)


Lesenswert?

Peter D. schrieb:
> In der Regel weiß man ja, ...
Ja. Aber man muss wissen warum.

Stefan U. schrieb:
> Wieso besser?
Es geht nicht darum, mal einfach [ aus Spaß ;) ] cli() und sei() 
aufzurufen. Es soll ein atomarer Zugriff erfolgen. Und da helfen die 
Makros aus der <util/atomic.h>, das ordentlich zu dokumentieren!

: Bearbeitet durch User
von dunno.. (Gast)


Lesenswert?

Ralf G. schrieb:
> Die korrekte Herangehensweise ist:
> - den Interruptstatus merken
> - Interrupts abschalten
> - de vorherigen Status wieder herstellen
>
> Dafür gibt's (mit etwas Schreibarbeit) Makros in der <util/atomic.h>.

Die lib kannte ich nicht.. Aber natürlich hast du vollkommen recht

Ich programmiere schon länger nur noch Ärmchen, da mache ich das 
tatsächlich auch immer genau so.

Vor allem wenn man irgendwelche Treiber für Peripheriezugriffe auch in 
ISRs nutzen muss, und evtl. übergeordnete Funktionen daher auch 
Interrupts ein/ausschalten müssen ists natürlich fatal, einfach immer 
sei(); zu nutzen.. Allerdings würde ich bei sowas dann lieber nur die 
tatsächlich gefährdeten Interrupts sperren..

von Peter D. (peda)


Lesenswert?

dunno.. schrieb:
> Allerdings würde ich bei sowas dann lieber nur die
> tatsächlich gefährdeten Interrupts sperren..

Würde ich gerade nicht.
Die ARM können ja Interruptlevel. Würde man nur einen Interrupt sperren, 
könnte sich ein langsamer und unwichtiger Interrupt vordrängeln und so 
den gesperrten zu lange verzögern.
Nur die globale Sperre sichert, daß es nicht zu einer 
Prioritätsinversion kommt. Sie verkürzt außerdem die Sperre auf die 
minimal mögliche Dauer.

von (prx) A. K. (prx)


Lesenswert?

Peter D. schrieb:
> Nur die globale Sperre sichert, daß es nicht zu einer
> Prioritätsinversion kommt. Sie verkürzt außerdem die Sperre auf die
> minimal mögliche Dauer.

Zumindest bei den Cortexen empfiehlt sich eine dritte Variante, die 
höher priorisierte Interrupts nicht behindert.

Die haben mit dem BASEPRI/BASEPRI_MAX Register eine sehr einfache 
Möglichkeit, den aktuellen Level auf den des betreffenden Interrupts zu 
setzen. Dann sind höher priorisierte Interrupts zulässig, alle anderen 
gesperrt.

Also sinngemäss:
  unsigned savedPRIO = BASEPRI;
  BASEPRI_MAX = meinePRIO;
  ... der betroffene Code ...
  BASEPRI = savedPRIO;
Dank spezieller Eigenschaften des BASEPRI_MAX Registers darf das auch 
verschachtelt werden, d.h. das funktioniert selbst dann, wenn im 
geschützten Code in einer aufgerufenen Funktion diese Sequenz mit einem 
anderen Level auftritt. Die Sequenz ist auch in ISRs zulässig.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

A. K. schrieb:
> Dann sind
> höher priorisierte Interrupts zulässig, alle anderen gesperrt.

Ich wüßte nicht, was das für einen Vorteil hätte. Es würde aber das 
Atomic Macro unnötig verkomplizieren.

Das BASEPRIE ist allerdings eine elegante Möglichkeit, das Atomic Macro 
zu realisieren.
Soweit ich mich erinnere, ist das globale Enable Flag privilegiert, d.h. 
ein Zugriff darauf mit erheblichem Aufwand verbunden.

von (prx) A. K. (prx)


Lesenswert?

Peter D. schrieb:
> Ich wüßte nicht, was das für einen Vorteil hätte.

Eigentlich denjenigen, den du selbst genannt hast: Wenn man alle 
Interrupts abschaltet, dann sind auch höher priorisierte Interrupts für 
diese Dauer gesperrt, d.h. werden entsprechend verzögert. Mit BASEPRI 
hingegen kommen die unverändert durch, d.h. werden in keinster Weise 
verzögert.

: Bearbeitet durch User
von Erleuchteter (Gast)


Lesenswert?

Falk B. schrieb:
> Solche Cast sind immer ein klares Zeichen von Mur

Klar, der AVR-GCC ist ja auch Murks

von Peter D. (peda)


Lesenswert?

A. K. schrieb:
> Wenn man alle
> Interrupts abschaltet, dann sind auch höher priorisierte Interrupts für
> diese Dauer gesperrt, d.h. werden entsprechend verzögert.

Ich nehme Atomic aber nicht dafür, ganze Unterfunktionen mit komplexen 
Berechnungen damit zu kapseln, sondern nur, um Daten konsistent zu 
halten, also einige LD oder ST Operationen lang. Da halte ich eine 
Unterbrechbarkeit für unnötig. Schon ein Interrupt-Prolog/-Epilog 
dürften länger brauchen, als nur ein paar LD/ST.

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.