Forum: Compiler & IDEs "static const" Objekt "wegoptimieren"?


von Markus G. (grabner)


Angehängte Dateien:

Lesenswert?

Hi!

Kann man den avr-g++ optimizer dazu bringen, ein Objekt, das "static 
const" deklariert ist, nicht im Speicher anzulegen, sondern den 
(konstanten) Inhalt des Objekts stattdessen quasi "inline" zu verwenden?

Im Anhang ist ein Beispiel, wozu das gut sein soll (ich bin gerade dabei 
zu evaluieren, wie man mit C++ für Atmel µC sauberen Code schreiben 
kann). Das Beispiel zeigt eine Klasse "PortPin", die einen Pin eines 
I/O-Ports repräsentiert und das entsprechende Bit setzen bzw. löschen 
kann. Wenn man den Code mit

avr-g++ -mmcu=atmega640 -O3 -S port.cc -o port.s

compiliert, bleibt von der test()-Funktion nur

        sbi 34-32,3
        cbi 34-32,3
        ret

übrig, weniger geht nicht. Nun wäre es aber wünschenswert, alle 
benötigten Port Pins in einem Header als "static const" Objekte zu 
definieren, also z.B.

static const PortPin LED1(PORTA, 0);
static const PortPin LED2(PORTA, 1);
// ...

Wenn man das macht, erhält man jedoch wesentlich mehr Code, die 
Test-Funktion im obigen Beispiel wird dann zu

        lds r30,ppa3
        lds r31,(ppa3)+1
        ld r24,Z
        lds r25,ppa3+2
        or r24,r25
        st Z,r24
        lds r30,ppa3
        lds r31,(ppa3)+1
        ld r25,Z
        lds r24,ppa3+2
        com r24
        and r24,r25
        st Z,r24
        ret

Außerdem wird Code erzeugt, um das Objekt im Speicher zu initialisieren. 
Eigentlich müsste der Compiler aber erkennen können, dass der Inhalt des 
Objekts konstant ist, und ähnlich optimalen Code wie oben erzeugen. 
Meine Frage ist daher, ob es irgendeine Compiler-Option (oder eine 
andere Formulierung im Code) gibt, damit auch mit "static const" 
Objekten optimaler Code erzeugt wird.

Mir ist klar, dass man mit dem Präprozessor etwas Ähnliches tun kann, 
z.B.

#define LED1 PortPin(PORTA, 0)
#define LED2 PortPin(PORTA, 1)
// ...

aber mir geht es hier darum auszuloten, wie weit man mit den 
Sprachelementen von C++ (und der optimizer-Implementierung im avr-g++) 
kommt. Hat jemand eine Idee dazu?

Danke & schöne Grüße,
Markus

von Markus G. (grabner)


Angehängte Dateien:

Lesenswert?

Markus Grabner schrieb:
> Wenn man den Code mit
>
> avr-g++ -mmcu=atmega640 -O3 -S port.cc -o port.s
>
> compiliert, bleibt von der test()-Funktion nur
>
>         sbi 34-32,3
>         cbi 34-32,3
>         ret
>
> übrig, weniger geht nicht.

Ich hatte versehentlich die Datei angehängt, die den langen Code 
erzeugt. Den kurzen Code (die drei Zeilen oben) erhält man mit der Datei 
"port_optimal.cc".

Schöne Grüße,
Markus

von Dr. Sommer (Gast)


Lesenswert?

Markus Grabner schrieb:
> aber mir geht es hier darum auszuloten, wie weit man mit den
> Sprachelementen von C++ (und der optimizer-Implementierung im avr-g++)
> kommt.
Das Problem ist, dass der Compiler zwar erkennen kann dass solche 
Variablen konstant sind und wegoptimiert werden können, aber das setzt 
sich erstmal nicht auf deren Membervariablen fort.
Seit C++11 gibt es aber den constexpr Mechanismus: Deklariere die 
Variablen statt "static const" als "constexpr", deklariere den 
Konstruktur als constexpr, und alle Memberfunktionen als "const" (das 
stellt bestimmte Anforderungen an den Code, siehe Google).
Dem GCC muss man noch sagen dass er C++11 verwenden soll, durch die 
Compileroption -std=c++11. Dazu sollte man mindestens Version 4.7.3 
verwenden. Eventuell muss man noch ein bisschen nachhelfen den Compiler 
zu überzeugen keine expliziten Funktionsaufrufe zu generieren, indem man 
die betreffenden Funktionen (d.h. Konstruktur & Member-Funktionen) mit 
"inline __attribute__((always_inline))" deklariert.
Dann sollte der Compiler beim Aufruf der Funktionen den Inhalt der 
Membervariablen kennen und das zusammen wegoptimieren.

von Dr. Sommer (Gast)


Lesenswert?

PS: noch ein paar Tips:
Anstelle von die "PORTx" Variable als Referenz in die Klasse zu 
übernehmen würde ich einen uint8_t der die Portnummer angibt verwenden 
(0 => PORTA, 1 => PORTB etc.), und dann "live" in den set/clear Methoden 
etc. die Adressen von den entsprechenden Registern PORTx, DDRx, PINx 
berechnen.
Wenn du die Instanzen dann als Konstanten verwendest wie jetzt wird das 
onehin komplett wegoptimiert, und so kannst du aber auch Instanzen an 
Funktionen übergeben (die dann ja nur 2 Bytes groß sind) wo dann 
"inline" eine Adressberechnung stattfindet (Dynamik/Flexibilität vs. 
Performance).
Die Übergabe sollte dabei ruhig by-Value stattfinden, das ist bei derart 
kleinen Klassen effizienter als by-Reference, da der zusätzliche 
Dereferenzierungsschritt wegfällt, die Übergabe aber dennoch effizient 
ist (direkt in Registern). Die Klasse PortPin ist dann quasi selber 
eine Referenz auf die entsprechenden Hardwareregister, und kann daher 
praktisch wie ein Pointer behandelt werden (wie eben Übergabe by-Value).

von Markus G. (grabner)


Angehängte Dateien:

Lesenswert?

Dr. Sommer schrieb:
> Markus Grabner schrieb:
>> aber mir geht es hier darum auszuloten, wie weit man mit den
>> Sprachelementen von C++ (und der optimizer-Implementierung im avr-g++)
>> kommt.
> Seit C++11 gibt es aber den constexpr Mechanismus: Deklariere die
> Variablen statt "static const" als "constexpr", deklariere den
> Konstruktur als constexpr, und alle Memberfunktionen als "const"
Super Tip, das war's schon! Die modifizierte Version ist wieder 
angehängt. Ich habe den gcc gleich mal von 4.3.3 auf 4.8.2 aktualisiert, 
der ist jetzt auch so freundlich, die Port-Adresse für den Assembler 
schon auszurechnen, also sieht die test()-Funktion jetzt so aus:

        sbi 0x2,3
        cbi 0x2,3
        ret

Irgendwie schräg, dass man ausgerechnet auf einem Mikrocontroller 
C++11-Features benötigt, um optimalen Code zu generieren :-)

Vielen Dank & schöne Grüße,
Markus

von Markus G. (grabner)


Lesenswert?

Dr. Sommer schrieb:
> Die Übergabe sollte dabei ruhig by-Value stattfinden, das ist bei derart
> kleinen Klassen effizienter als by-Reference, da der zusätzliche
> Dereferenzierungsschritt wegfällt, die Übergabe aber dennoch effizient
> ist (direkt in Registern).
Grundsätzlich stimme ich zu, aber nachdem der Compiler/Optimizer mit der 
Referenz klarkommt, gefällt es mir hier aus Gründen der Lesbarkeit 
besser, direkt die PORTx-Konstanten zu verwenden. Wer weiß, was PORTA 
(und constexpr :-) bedeutet, wird vermutlich auch

constexpr PortPin LED1(PORTA, 0);

verstehen, ohne in der Dokumentation nachlesen zu müssen.

Schöne Grüße,
Markus

von Roland (Gast)


Lesenswert?

Das muss am Datentyp "PortPin"  liegen[1].
Definiere ich im IAR (für den MSP) einen "const char" (int, long etc) 
landet der ordnungsgemäß im Flash und wird nicht in den RAM gespiegelt.


[1]Das Dilemma wird wohl das wüste Durcheinander von Makros und 
Variablen sein, welches AVR mit seinen Headern ausliefert.

von Dr. Sommer (Gast)


Lesenswert?

Markus Grabner schrieb:
> Grundsätzlich stimme ich zu, aber nachdem der Compiler/Optimizer mit der
> Referenz klarkommt, gefällt es mir hier aus Gründen der Lesbarkeit
> besser, direkt die PORTx-Konstanten zu verwenden.
Das "Problem" tritt halt auf wenn du außer dem PORTx Register noch DDRx 
und PINx hinzufügst, und eine PortPin Instanz in einem Kontext 
verwendest, in dem der Compiler deren Inhalt nicht kennt (zB. 
Funktions-Parameter oder allgemein veränderbare Variable). Dann wird die 
komplette Instanz inkl. 3er Referenzen und Bitmaske kopiert & übergeben 
bzw. gespeichert. Das verwenden der Nummer würde diesen Speicher 
einsparen.

Markus Grabner schrieb:
> Wer weiß, was PORTA
> (und constexpr :-) bedeutet,
... richtig. Aber das kann man wiederum ausgleichen über eine 
vordefinierte Factory-Klasse und entsprechende Instanzen:
1
class PortFactory {
2
  public:
3
    inline constexpr PortFactory (uint8_t nPort) : m_nPort (nPort) {}
4
    inline constexpr PortPin operator [] (uint8_t i) const { return {m_nPort, 1 << i}; }
5
  protected:
6
    uint8_t m_nPort;
7
};
8
9
constexpr PortFactory PORTA (0);
10
constexpr PortFactory PORTB (1);
11
constexpr PortFactory PORTC (2);
12
13
constexpr PortPin LED1 { PORTC[3] };
14
15
int main () {
16
  PORTA [0].set ();
17
  PORTB [5].clear ();
18
  LED1.set ();
19
}

Die PORTA,B,C,LED1 Instanzen werden hier natürlich wieder wegoptimiert, 
falls möglich.

Markus Grabner schrieb:
> Irgendwie schräg, dass man ausgerechnet auf einem Mikrocontroller
> C++11-Features benötigt, um optimalen Code zu generieren :-)
Warum nicht, in C++ werden neue innovative Features zur 
Effizienz-Verbesserung auch bei abstrakteren Programmen (wie eben mit 
eigenen Klassen) eingebaut. u.a. dank constexpr eignet sich C++ umso 
mehr für Mikrocontroller.

Roland schrieb:
> Definiere ich im IAR (für den MSP) einen "const char" (int, long etc)
> landet der ordnungsgemäß im Flash und wird nicht in den RAM gespiegelt.
Ja, genau das ist das Problem. Die Konstante soll direkt in die 
Instruktionen (sbi etc) encodiert werden, und nicht als 
Extra-Konstante im Flash (oder RAM) die erst per "lpm"("lds") geladen 
werden müsste.

Roland schrieb:
> Das muss am Datentyp "PortPin"  liegen[1].
Den hat er selbst definiert.

Roland schrieb:
> [1]Das Dilemma wird wohl das wüste Durcheinander von Makros und
> Variablen sein, welches AVR mit seinen Headern ausliefert.
Das Problem ist der alte C++03 Standard, welcher kein constexpr kennt 
und somit keine Möglichkeit, Compile-Time-Konstanten dem Compiler & 
Optimizer bekannt zu machen. Dass es in der 1. Version der 
"port_optimal.cc" "richtig" funktionierte ware pure Freundlichkeit vom 
GCC, das so zu optimieren.

von Roland (Gast)


Lesenswert?

Dr. Sommer schrieb:
> ...
>
> Roland schrieb:
>> Definiere ich im IAR (für den MSP) einen "const char" (int, long etc)
>> landet der ordnungsgemäß im Flash und wird nicht in den RAM gespiegelt.
> Ja, genau das ist das Problem. Die Konstante soll direkt in die
> Instruktionen (sbi etc) encodiert werden, und nicht als
> Extra-Konstante im Flash (oder RAM) die erst per "lpm"("lds") geladen
> werden müsste.
> ...

Da der (embedded-)Kern (welcher auch immer) sowieso alles was er macht 
erst mal aus dem Speicherort in irgend welche Arbeitsregister laden 
muss, verstehe ich dein Problem nicht. Es ist völlig Latte, ob lpm die 
Arbeitsdaten aus einer Flash-Zelle oder Ram-Zelle holt. Es dauert immer 
einen Takt. Das ist der Preis der Harvard-Architektur.

Dem OP ging es doch nur darum, das "doppelte" Laden vom Flash in eine 
Ram-Zelle und dann erst in die Arbeitsregister zu vermeiden. Zumindest 
habe ich es so interpretiert. Denn alle weiterführenden 
Optimierungsversuche scheitern an der Arbeitsweise der CPUs.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Du willst doch jetzt nicht behaupten, daß der Flash- oder gar 
RAM-Verbrauch egal wären und man nicht drauf achten sollte?

von Roland E. (roland0815)


Lesenswert?

Wenn man die volle Optimierung vom Kompiler zulässt: Ja, es ist egal, 
denn der Kompiler wird das gewünschte Optimum (Größe/Geschwindigkeit) 
rausholen. Wieviel man letztlich braucht, entscheidet der Programmierer, 
wie "verschwenderisch" er mit Variablen umgeht. Wenn ich keine Variablen 
anlege, brauche ich auch keinen RAM.

Das eigentliche Problem des OP entsteht aber eh aus der Plattform AVR, 
da der Kopfstände machen muss, um Daten aus dem Programmspeicher auch 
als Daten verarbeiten zu können. Andere Plattformen brauchen diese 
Aufstände nicht. Dort reicht tatsächlich ein "static const" um Ram zu 
sparen. Mehr Flash braucht man dabei auch nicht, denn alle Konstanten, 
auch wenn sie später im RAM liegen müssen ja trotzem erst mal im Flash 
liegen.

von Dr. Sommer (Gast)


Lesenswert?

Roland schrieb:
> Da der (embedded-)Kern (welcher auch immer) sowieso alles was er macht
> erst mal aus dem Speicherort in irgend welche Arbeitsregister laden
> muss, verstehe ich dein Problem nicht.
Das Laden direkt aus einem Opcode geht schneller als das Laden per lpm.

> Es ist völlig Latte, ob lpm die
> Arbeitsdaten aus einer Flash-Zelle oder Ram-Zelle holt.
Bis darauf dass lpm ausschließlich auf dem Flash funktioniert, und 
nicht mit dem RAM.
> Es dauert immer einen Takt. Das ist der Preis der Harvard-Architektur.
Nein, LPM braucht 3 Zyklen, LD 2.

Roland schrieb:
> Dem OP ging es doch nur darum, das "doppelte" Laden vom Flash in eine
> Ram-Zelle und dann erst in die Arbeitsregister zu vermeiden.
Es ging ihm hauptsächlich darum, die Zieladresse in den Opcode zu 
encodieren, anstelle sie als seperate Konstante im Flash zu haben.

Roland schrieb:
> Denn alle weiterführenden
> Optimierungsversuche scheitern an der Arbeitsweise der CPUs.
Wie man sieht kann man die CPU mehr oder weniger optimal nutzen.

Zur Verdeutlichung etwas Code (modulo Syntaxfehler):
Dies passiert, wenn die Portadresse & Pinmaske als Konstanten im Flash 
liegen und der Compiler sie beim Pinsetzen nicht kennt - er muss sie 
immer extra laden:
1
.CSEG
2
; globale "static const" Variablen im Flash
3
PORTADRESSE: .DW PORTA   ; Portadresse im Flash
4
PINMASK: .DB 2           ; Pinmaske im Flash
5
 ...
6
7
; Code zum Setzen der Bits anhand der Variablen im Flash
8
ldi zl, low(PORTADRESSE)   ; Lade Portadresse ...
9
ldi zh, high(PORTADRESSE)
10
lpm yl, Z+
11
lpm yh, Z+
12
13
ldi zl, low(PINMASK)    ; Lade Pinmaske...
14
ldi zh, high(PINMASK)
15
lpm r17, Z
16
ld r18, Y     ; Lade PORT-Wert
17
or r18, r17   ; Setze Maske
18
st r18, Y   ; Setze PORT-Wert

Wenn dem Compiler aber hingegen die Inhalte der Portadresse & Pinmaske 
zum Zeitpunkt des Setzens bekannt sind, kann er diese direkt in die 
Instruktion encodieren und folgendes generieren:
1
sbi PORTA, 1
Wenn man genau hinsieht kann man einen Größen&Laufzeit -Unterschied 
erkennen, obwohl beides Male die Portadresse & Pinmaske aus dem Flash 
stammen.

Roland Ertelt schrieb:
> Wenn man die volle Optimierung vom Kompiler zulässt: Ja, es ist egal,
> denn der Kompiler wird das gewünschte Optimum (Größe/Geschwindigkeit)
> rausholen.
Das kann er hier nicht, wenn er den Port nicht kennt! Dann muss er ihn 
"dynamisch" aus der Konstanten-Tabelle (im Flash) laden (siehe 1. 
Beispiel).

Roland Ertelt schrieb:
> Wieviel man letztlich braucht, entscheidet der Programmierer,
> wie "verschwenderisch" er mit Variablen umgeht.
Richtig - ohne constexpr wird eine konstante Variable im Flash angelegt 
die extra geladen werden muss.
> Wenn ich keine Variablen
> anlege, brauche ich auch keinen RAM.
... oder Flash; und genau darum gehts bei constexpr, das Anlegen einer 
expliziten konstanten Variablen (ja, so heißt das in C, C++) zu 
verhindern und stattdessen die Daten direkt in den Opcode zu 
integrieren.

Roland Ertelt schrieb:
> Das eigentliche Problem des OP entsteht aber eh aus der Plattform AVR,
> da der Kopfstände machen muss, um Daten aus dem Programmspeicher auch
> als Daten verarbeiten zu können.
Diese Plattform macht den unoptimierten Code besonders hässlich, ja - 
aber das Ursprungsproblem kommt aus der Compiler&Sprach -Logik.

Roland Ertelt schrieb:
> Dort reicht tatsächlich ein "static const" um Ram zu
> sparen.
Er möchte aber auch Flash sparen.
> Mehr Flash braucht man dabei auch nicht, denn alle Konstanten,
> auch wenn sie später im RAM liegen müssen ja trotzem erst mal im Flash
> liegen.
Wenn sie aber in der Instruktion liegen (siehe 2. Beispiel) brauchen sie 
weniger Flash.

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.