Forum: Compiler & IDEs Problem SRAM Optimierung AVR-GCC


von Peter D. (peda)


Lesenswert?

Ich unterteile ja gern meinen Code in kleine Funktionen.
Das Problem ist aber, daß der SRAM-Verbrauch im AVR dann sehr stark 
ansteigt.

Der GCC scheint ziemlich feste Ansichten zu haben, welche Register was 
machen, obwohl die ja fast universell austauschbar sind. Außerdem geht 
er immer davon aus, daß jede Funktion alle "Call-used registers" 
zerstört.

Eine Registeroptimierung scheint nicht stattzufinden und er PUSH, POP, 
MOVW in jeder Funktion sehr viel unnütz.

Ich habe z.B. das Problem, daß bei nur 2kB Code schon 70Byte SRAM nur 
fürs (unnütze) Register retten draufgeht, ist bei 256 Byte SRAM ne ganze 
Menge.


Ich hab nun versucht, mit "-fwhole-program" die Call-Barrieren 
aufzulösen, um den SRAM-Bedarf zu optimieren.
Es geht allerdings nur, wenn ich alle C-Files in ein einziges 
includiere.
Ansonsten kriege ich Fehlermeldungen, z.B.:
1
F:\WORK\AVR_C\AUTORANG/main.c:31: undefined reference to `eeprom_rw'
2
F:\WORK\AVR_C\AUTORANG/main.c:52: undefined reference to `checker'
3
MAKE.EXE: *** [main1.elf] Error 1


Komischer Weise hat das aber auch keine RAM-Einsparung gebracht, da er 
plötzlich im Main erstmal etwa 20 Register PUSHt.
Mit "__attribute__((noreturn))" kriege ich die 20 Bytes zwar wieder 
frei, aber dann warnt er blöd rum. Kann man die Warnung abschalten?


Ich muß wohl dem GCC zuliebe das modulare Programmieren aufgeben und 
alles in einen einzigen langen Spaghetticode pappen.

Hat vielleicht jemand gute Tips bezüglich SRAM/Registeroptimierung?
Gibts da vielleicht irgendwelche geheimen Schalter?


Peter


P.S.:
Ich benutze den aktuellen WINAVR GCC 4.3.2

von Stefan E. (sternst)


Lesenswert?

Peter Dannegger wrote:

> Mit "__attribute__((noreturn))" kriege ich die 20 Bytes zwar wieder
> frei, aber dann warnt er blöd rum. Kann man die Warnung abschalten?

Schon mit dem Attribut "OS_main" statt "noreturn" versucht?

Bei wirklich kleinen Funktionen wäre es vielleicht auch eine Überlegung 
wert, sie als static inline in ein Header-File zu packen.

von Εrnst B. (ernst)


Lesenswert?

Hab das bei mir so gelöst:

im Makefile
CFLAGS+=-ffreestanding
dann warnt der GCC nicht mehr, wenn main kein int mehr returnt.

und im main.c:
1
void __attribute__ ((noreturn)) main(void);
2
void main(void) {
3
4
...

Und schon geht das ganze ohne Meckerrei durch den Compiler.

von Stefan E. (sternst)


Lesenswert?

Man sollte sich nur klar darüber sein, dass man dem Compiler mit 
-ffreestanding auch einige Optimierungsmöglichkeiten nimmt. Das 
klassische Beispiel ist ein strlen("..."), das dann nicht mehr durch 
eine Konstante ersetzt werden kann.

von Peter D. (peda)


Lesenswert?

Stefan Ernst wrote:
> Das
> klassische Beispiel ist ein strlen("..."), das dann nicht mehr durch
> eine Konstante ersetzt werden kann.

Meinst Du, wenn man statt strlen auch sizeof nehmen könnte?


Peter

von Stefan E. (sternst)


Lesenswert?

Peter Dannegger wrote:

> Meinst Du, wenn man statt strlen auch sizeof nehmen könnte?

Klar kann man auch sizeof verwenden, wenn du entsprechend 
berücksichtigst, dass sizeof einen anderen Wert liefert. 
Selbstverständlich könnte man auch gleich eine Zahl hinschreiben.

von Εrnst B. (ernst)


Lesenswert?

Hmm. Gut, ich benutz kein strlen, aber unschön ist das doch,

Gibts eine Möglichkeit dem GCC das Meckern wg "void main(void)" 
schonender abzugewöhnen?

von Stefan E. (sternst)


Lesenswert?

Ernst Bachmann wrote:

> Gibts eine Möglichkeit dem GCC das Meckern wg "void main(void)"
> schonender abzugewöhnen?

Ich sehe die Notwendigkeit nicht. Ein Standard-main zusammen mit dem 
Attribut OS_main sollte doch das Gewünschte liefern.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Peter Dannegger wrote:
> Der GCC scheint ziemlich feste Ansichten zu haben, welche Register was
> machen, obwohl die ja fast universell austauschbar sind. Außerdem geht
> er immer davon aus, daß jede Funktion alle "Call-used registers"
> zerstört.
Klar. Die hat jeder Compiler und muss sie haben. Das heißt ABI.
Eine Funktion ist eine Black Box. Der Compiler kennt den Code ja nicht 
und muss davon ausgehen, daß die Register zerstört werden. Dem 
entsprechend muß er, wenn lokale Variablen einen Call überleben müssen, 
diese sichern. Das kann nur dadurch geschehen, indem sie in 
call-saved-regs gesichert werden, aber deren Inhalt muss ihrerseit 
gesichert werden, nämlich auf dem Stack. ALternativ können Werte direkt 
auf dem Stack gesichert werden, oder eine Variable wird erst garnicht in 
einem GPR angelegt, sondern direkt im Frame der Funktion.

Und bestimmte Register haben bestimmte Funktionen. Adress-Register sind 
sehr explusiv, im Endeffekt bleibt bei komplexen Funktionen nur Z (Y ist 
framepointer) und X kann keine Offsets adressieren.

Ebenso muss natürlich festgelegt sein, in welchen Registern eine 
Funktion ihre Argumente erhält und wo der Return-Wert zu liefern ist.

> Eine Registeroptimierung scheint nicht stattzufinden und er PUSH, POP,
> MOVW in jeder Funktion sehr viel unnütz.

Doch, schon. Aber je komplexer eine Funktion ist, desto mehr muss 
gesichert werden. Unangenehm können da Optimierungen wie constant 
propagation oder invariant motion wirken.

> Ich hab nun versucht, mit "-fwhole-program" die Call-Barrieren
> aufzulösen, um den SRAM-Bedarf zu optimieren.
> Es geht allerdings nur, wenn ich alle C-Files in ein einziges
> includiere.
> Ansonsten kriege ich Fehlermeldungen, z.B.:
>
1
> F:\WORK\AVR_C\AUTORANG/main.c:31: undefined reference to `eeprom_rw'
2
> F:\WORK\AVR_C\AUTORANG/main.c:52: undefined reference to `checker'
3
> MAKE.EXE: *** [main1.elf] Error 1
4
>
Es sind alle Quellen anzugeben. Offenbar enthält main.c externe 
Referenzen, zB für checker.

> Ich muß wohl dem GCC zuliebe das modulare Programmieren aufgeben und
> alles in einen einzigen langen Spaghetticode pappen.
>
> Hat vielleicht jemand gute Tips bezüglich SRAM/Registeroptimierung?
> Gibts da vielleicht irgendwelche geheimen Schalter?

Du kannst Funktionen inlinen, was zu einem stark wachsenden Code führen 
dürfte. Einen Tod musst du dann sterben,...
Was sinnvoll ist zum Inlinen musst du erst mal rausfinden. Funktionen 
die zB nur triviale Sachen machen wie set_flag(char f) {flag=f;}
bringen natürlich einen ziemlichen Overhead, wenn sie nicht geinlint 
werden, man muss sich also überlegen welche Granulierung in Funktionen 
überhaupt sinnvoll ist. Eine Aufruf foo() kostet immer gleich viel an 
Code, egal wie komplex foo intern ist, denn der Compiler kennt den Code 
nicht. U.U existiert der Code nocht nichtmal und das Modul, das foo 
implementiert, wird erst später erstellt.

Funktionen sollten auch so geschrieben werden, daß möglichst kein Frame 
gebraucht wird. Für komplexe Funktionen wird der natürlich gebraucht, 
aber für einfache Funktionen gibt's ein paar Dinge, die man unbedingt 
meiden sollte. Ohne die (präcompilierte) Quelle zu sehen, kann man da 
aber nix weiter zu sagen.

Johann

von (prx) A. K. (prx)


Lesenswert?

Zu den Frames gibt schon einen Tipp:

Wenn man von einer lokalen Variablen die Adresse verwendet, und die ist 
nicht "static", dann hat man einen kompletten Stackframe gewonnen. Und 
da Atmel sich auch noch bei den xmegas hartnäckig weigert, einen Befehl 
zur atomaren Manipulation des Stackpointers zu definieren, ist der Frame 
etwas aufwendig (von IAR geschmiert? wäre ja nicht schwierig gewesen).

Das betrifft einerseits lokale Arrays wie Stringpuffer. Macht man die 
"static", geht das nur auf andere Weise zu Lasten des RAM-Verbrauchs. 
Man kann auch die Puffer mehrerer solcher Funktionen zusammenfassen, 
aber die programmtechnische Eleganz (Kapselung) lässt dann etwas nach.

Lokale "struct"s sind natürlich auch solche Kandidaten, aber das ist in 
der Praxis bei Programmierung kleiner µCs eher bei C++ als bei C ein 
Thema.

Vor allem aber bedeutet es, soweit wie möglich auf Rückgabewerte per 
Pointer zu verzichten. Wer beispielsweise eine Funktion zur 
Temperaturmessung so schreibt, dass der Returnwert nur OK/Fehler anzeigt 
und der Temperaturwert über einen Adressparameter zurückgegeben wird, 
der hat schon im Ansatz verloren. Hier geht diese Strategie also schon 
sehr früh ins Programmdesign ein. Effizienter ist es hier, für "Fehler" 
nicht vorkommende Temperaturwerte zu missbrauchen (wie -300°C).

Johanns skizzierte "set_flag" Funktion ist jedoch vergleichsweise 
harmlos. Einen Frame kriegt man dabei keinen, nur die volatilen Register 
sind seitens des Callers natürlich weg.

Lange push/pop-Orgien verkürzt man mit -mcall-prologues. Braucht nicht 
weniger RAM, sieht aber im Listing nicht so krass aus.

Ein Frame wird billiger, indem man den Stack auf eine 256-Byte Seite 
einschränkt: -mtiny-stack. Dabei sollte man dann aber beachten, dass je 
nach Controller der Stack nicht bei xxFF anfängt, sondern woanders.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

A. K. wrote:
> Zu den Frames gibt schon einen Tipp:
>
> Wenn man von einer lokalen Variablen die Adresse verwendet, und die ist
> nicht "static", dann hat man unweigerlich einen kompletten Stackframe
> gewonnen.

Gleiches gilt für Übergabeparameter. Von einem Register kann keine 
Adresse genommen werden. Selbst wenn ein µC das bietet (AVR ist ein 
Fall) wird das vom Compiler nicht unterstützt, und es ist auch nicht 
sinnvoll, das zu unterstützen.

> Und da Atmel sich hartnäckig weigert, einen Befehl zur
> atomaren Manipulation des Stackpointers zu definieren, ist der Frame
> etwas aufwendig.

Ja sowas würde man sich natürlich wünschen, den SP atomar und einfach in 
einer Instruktion verschiebbar zu machen. Platz in den Opcodes wäre da.

> Das betrifft einerseits lokale Arrays wie Stringpuffer. Wenn's geht
> macht man die "static", was aber zu Lasten des RAM-Verbrauchs geht. Man
> kann auch die Puffer mehrerer solcher Funktionen zusammenfassen, aber
> die programmtechnische Eleganz (Kapselung) lässt dann etwas nach.

> Lokale "struct"s sind natürlich auch solche Kandidaten, aber das ist in
> der Praxis bei Programmierung kleiner µCs eher bei C++ als bei C ein
> Thema.

Ich arbeite recht viel mit Structs/Unions in C, auch zur 
Parameterübergabe und als lokale Variablen. Zu beachten ist aber, daß 
eine solche nur in einem Register angelegt werden kann, wenn sie eine 
gerade Größe hat, also 1, 2 oder 4 Bytes. Zu einer Struktur, die 
RGB-Farbwerte enthält, kann man also gewinnbringend ein Dummy-Feld 
hinzufügen (falls man keine großen Arrays darauf aufbaut).

> Vor allem aber bedeutet es, soweit wie möglich auf Rückgabewerte per
> Pointer zu verzichten. Wer beispielsweise eine Funktionen zur
> Temperaturmessung so schreibt, dass der Returnwert nur OK/Fehler anzeigt
> und der Temperaturwert über einen Adressparameter zurückgegeben wird,
> der hat schon im Ansatz verloren.

Du meinst sowas?
1
int foo (int*);
Schlimm ist das ja nicht, auch wenn es per Return günstiger wird. Übel 
wir nur die Call-Seite, wenn eine lokale Variable referenziert wird:
1
void bar (int x)
2
{
3
    foo (&x);
4
}

Wenn man hingegen Adressen von globalenb Variablen transportiert, kann 
so ein Ansatz zu einem deutlichen Code-Schrumpf führen.

> Lnge push/pop-Orgien verkürzt man mit -mcall-prologues.

Das macht den Code u.U kleiner. Aber spart das RAM? Seh ich jetzt nicht. 
Weniger als die notwendigen REGs wird gcc nicht sichern. U.U wird er 
aber mehr sichern als notwendig, wenn die zu sichernde reg-sequence ein 
Loch hat.

Johann

von (prx) A. K. (prx)


Lesenswert?

Johann L. wrote:

> Du meinst sowas?
>
1
> int foo (int*);
2
>
> Schlimm ist das ja nicht, auch wenn es per Return günstiger wird.

Für diese Funktion selbst nicht. Nur wird in 90% der Fälle der Caller 
eine lokale Variable verwenden, und dann ist's aus. Da ist es 
sinnvoller, sowas gleich vorneweg im Design zu vermeiden, statt sich 
hinterher beim Aufruf einen abbiegen zu müssen.

Das heisst beispeilsweise auch, dass es bei Dingern wie den 
Temp/Feuchte-Sensoren SHT11 sinnvoller sein kann, getrennte Funktionen 
für Temperatur und Feuchte zu implementieren. Das zusammenzufassen 
bringt bei AVRs eher Nachteile.

> Das macht den Code u.U kleiner. Aber spart das RAM?

Nö, aber passte da grad rein ;-). Ist mehr Psychologie, jene 
adressierend, denen schon beim Anblick der push/pop-Orgien schlecht 
wird.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

A. K. wrote:
> Johann L. wrote:
>
>> Du meinst sowas?
>>
1
>> int foo (int*);
2
>>
>> Schlimm ist das ja nicht, auch wenn es per Return günstiger wird.
>
> Für diese Funktion selbst nicht. Nur wird in 90% der Fälle der Caller
> eine lokale Variable verwenden, ...
Wie gesagt, ich verwende es mit globalen Variablen um Codegröße zu 
sparen. Allerding nicht mit int als Basistyp. Aber das führt jetzt zu 
weit, weil das Code spart, aber kein RAM (und wenn, dann eher als 
angenehmer Nebeneffekt).

Johann

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Peter Dannegger wrote:
> Ich unterteile ja gern meinen Code in kleine Funktionen.

Die Frage ist auch, was "klein" ist, und in welchen Kontext das dann 
aufgerufen wird.

> Komischer Weise hat das aber auch keine RAM-Einsparung gebracht, da er
> plötzlich im Main erstmal etwa 20 Register PUSHt.
> Mit "__attribute__((noreturn))" kriege ich die 20 Bytes zwar wieder
> frei, aber dann warnt er blöd rum. Kann man die Warnung abschalten?

Ich lasse main wie es ist und mache die Hauptschleife in einer 
noreturn-Funktion. Die main-Loop und den ganzen Kladderatddatsch will 
man eh nicht in main haben. Das initialisiert die Hardware und ab geht's 
in die Hauptschleifen-Func.

> Hat vielleicht jemand gute Tips bezüglich SRAM/Registeroptimierung?
> Gibts da vielleicht irgendwelche geheimen Schalter?

"Geheim" sind -morder1 und -morder2, die Einfluß auf die 
Register-Allokierung haben.

Mit -morder1 wird der Code manchmal kleiner, manchmal nicht. Das für 
jede Funktion/Modul auszuwünscheln hab ich mir nicht den Wolf gemacht. 
Nur einmal den Build-Prozess abhängig vom der Modulgröße gemacht: Für 
einen bestimmten Satz von Optionen jeweils alle Object-Varianten 
auscompiliert und das kleinste Modul gelinkt. Das ist eine Fingerübung 
in make.

Inwieweit eine andere regalloc-order in der Lage ist, die Anzahl zu 
sichernder Register zu reduzieren, hab ich nicht untersucht. Ich würd 
aber mal schätzen, daß es kaum Einfluß hat, weil es die gleiche ABI 
implementiert. Und diese Option ist wie gesagt undokumentiert -- sie 
diente den Entwicklern des avr-Backend in der Entwicklungsphase zum 
Eruieren einer guten regalloc-order und ist im Backend drinne, weil es 
(noch) niemand entfernt hat.

Mit -morder2 hab ich noch nie kleineren Code gesehen.

Prinzipiell wäre es möglich, für einfache Funktionen ein anderes 
call-Interface zu implementieren, indem man eine Funktion, von der man 
weiß (oder hofft), daß sie nur simple Sachen macht und nach Möglichkeit 
eine leaf-Funktion ist, per __attribute__((simple)) kennzeichnet.

Für eine Funktion kann der Compiler zwar erkennen, daß sie "simpel" ist 
(wie immer man das definiert), der Caller muss aber zu seiner 
Compilezeit wissen, daß der Callee "simpel" ist! Daher müsste man ein 
solches Attribut einführen, um das dem Caller per Prototyp des Callee 
mitzuteilen. Falsche Attribuierung führt dann übrigens klar zum Crash 
(wie es i.d.R eben ist bei falschen Prototypen).

Johann

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


Lesenswert?

A. K. wrote:

> Und
> da Atmel sich auch noch bei den xmegas hartnäckig weigert, einen Befehl
> zur atomaren Manipulation des Stackpointers zu definieren, ist der Frame
> etwas aufwendig (von IAR geschmiert? wäre ja nicht schwierig gewesen).

Naja, mit seinerzeit IAR als einzigem Berater ist keiner auf die
Idee gekommen, dass man statt zwei Stacks (von denen ja einer immer
präalloziert sein muss) auch alles in einem Stack unterbringen kann,
würde ich mal sagen.  Wenn man zeitlebens nur mit zwei Stacks gearbeitet
hat und nie ein Unix gesehen hat (die ja praktisch alle einen Stack
benutzen), dann kommt man möglicherweise nicht auf die Idee...

Mittlerweile gibt's da im Prolog teilweise "rcall .".  Ich hatte
zuerst an einen Bug geglaubt... ist aber weiter nichts, als auf dem
Stack zwei (bzw. drei) Bytes frei zu räumen. :-o

von Peter D. (peda)


Lesenswert?

@Stefan,

> Schon mit dem Attribut "OS_main" statt "noreturn" versucht?

Ja, das hats gebracht.
Keine PUSHs und keine Warnung, super danke.

> Bei wirklich kleinen Funktionen wäre es vielleicht auch eine Überlegung
> wert, sie als static inline in ein Header-File zu packen.

Ja, ich hab jetzt mal die Module etwas größer gemacht (z.B. I2C und 
EEPROM zusammen), und dann die Unterfunktionen always inline.
Hat etwa 15 PUSHs eleminiert und 4 Calls.

@Johann

> Klar. Die hat jeder Compiler und muss sie haben. Das heißt ABI.
> Eine Funktion ist eine Black Box. Der Compiler kennt den Code ja nicht
> und muss davon ausgehen, daß die Register zerstört werden.

Z.B. der Keil C51 erstellt sich ein Registerfile, wo die verwendeten 
Register einer Funktion mit einem Flag gekennzeichnet ist.
Beim 2. Durchlauf kann er dann die Aufrufer entsprechend optimieren, 
bzw. noch weitere Durchläufe machen.

Bzw. wenn die aufgerufenen Funktionen vor dem Aufrufer stehen, kennt er 
diese ja schon und optimiert natürlich gleich im 1.Durchlauf.
Diese Optimierung müßte eigentlich auch der GCC können.


>> F:\WORK\AVR_C\AUTORANG/main.c:52: undefined reference to `checker'
>> MAKE.EXE: *** [main1.elf] Error 1
> Es sind alle Quellen anzugeben. Offenbar enthält main.c externe
> Referenzen, zB für checker.

Dann dürfte es ja ohne "-fwhole-program" auch nicht gehen, da geht es 
aber.
Ich muß auch nicht sämtliche Objekte includieren, einige können durchaus 
separat compiliert werden.
Ich werd mal nen Beispielcode zusammenstellen und posten.


Peter

von Stefan E. (sternst)


Lesenswert?

Peter Dannegger wrote:

> Ja, ich hab jetzt mal die Module etwas größer gemacht (z.B. I2C und
> EEPROM zusammen), und dann die Unterfunktionen always inline.
> Hat etwa 15 PUSHs eleminiert und 4 Calls.

Nur so als Hinweis:
Das Zusammenfassen zu größeren Modulen ist nicht unbedingt nötig. Du 
kannst die gemeinsamen Unterfunktionen auch in eine separate Datei 
auslagern und dann in beide Module inkludieren. Du darfst halt nur nicht 
vergessen, diese Unterfunktionen dann als static zu deklarieren.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Peter Dannegger wrote:
>>> F:\WORK\AVR_C\AUTORANG/main.c:52: undefined reference to `checker'
>>> MAKE.EXE: *** [main1.elf] Error 1
>> Es sind alle Quellen anzugeben. Offenbar enthält main.c externe
>> Referenzen, zB für checker.
>
> Dann dürfte es ja ohne "-fwhole-program" auch nicht gehen, da geht es
> aber.

Natürlich kannst du ohne -fwhole-program externe Referenzen nutzen, ohne 
daß es zu Problemen im Compiler kommt. Diese werden erst vom Linker 
aufgelöst.

Aber wenn du über den Schalter sagst, "hey, hier ist das komplette 
Programm" und dann fehlt was, wird eben gemeckert.

Ebenso würde bei unauflösbaren externen Referenzen gemeckert werden, 
wenn diese ohne -fwhole-program beim Linken/Lokatieren auftreten würden.

Johann

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.