Hallo,
mein aktuelles Hobbyprojekt für einen Mega8 drohte über die
Flash-Kapazität hinauszuwachsen, 6 KB in meinem Fall, denn 2 KB gehen
für meinen Bootloader drauf. Nach ein paar Maßnamen paßt es nun aber
wieder komfortabel hinein, ich will hier mal meine Erfahrungen teilen.
Ausganspunkt war "normaler" Embedded-Code, schon ziemlich dicht codiert,
das ist nichts Neues für mich, damit verdiene ich auch mein Geld. Nichts
speziell auf Größenoptimierung "verunstaltet" (jeden gemeinsamen Fitzel
Code in eine Unterfunktion o.Ä.). Das habe ich auch in Folge nicht
getan. Keine üblichen "Anfängerfehler" drin, keine floats, printf's,
unnötige Libs, malloc, Strings, etc. Immer schön ins map-File gucken, ob
man die aufgeführten Library-Funktionen auch wirklich haben wollte.
- gcc v4.1.1 versus v3.4.6: meistens ist der alte Compiler besser, in
meinem Fall momentan um 40 Byte. Den Bootloader hingegen macht gcc 4.1.1
kleiner. Hilft mir in dem Fall aber nicht, unter die nächste Grenze von
1KB kriege ich den nie.
- Nullinitialisierung von statischen Variablen: Bisher hatte ich in
meinen Init-Funktionen nicht auf den Startup-Code verlassen, sondern die
nötigen Variablen genullt. So kann ich auch eine Re-Initialisierung
machen, habe aber keinen Gebrauch davon gemacht. Stattdessen starte ich
komplett neu. Entfernung Null-Inits brachte einiges. (OK, kann man doch
als Anfängerfehler bezeichnen, ich mußte erst im C-Standard nachlesen
daß das BSS-Nullen keine Eigenart von gcc ist.)
- statische (globale) Variablen in ein struct sammeln: Das erleichtert
dem Compiler die Adressierung, da er den Basiszeiger wiederverwenden
kann. Finde ich aber schwach, daß der Compiler sich das nicht selbst
passend hinlegt. Die Codegröße kann dann noch von der Reihenfolge der
struct-Member abhängen Die häufigst benutzte Variable sollte am Anfang
stehen, dann kann sie ohne Offset direkt mit dem Basiszeiger adressiert
werden. Ansonsten in Gruppen, wie die Variablen auch gebraucht werden.
Hier kann man viel rumprobieren...
- Multiplikationen mit Konstanten: der Compiler instanziiert sofort eine
teure allgemeine Bibliotheksfunktion, auch wenn es anders ginge. Ich
hatte eine einzige 32-bit Multiplikation mit 10 drin, die mir ein mulsi3
beschert hat. Mit a = b<<3 + b<<1 geht es in dem Fall kürzer. Wie
gesagt, map-File beobachten.
- alle Variablen nur so breit wie nötig: Hatte ich eigentlich schon, nur
an einigen wenigen Stellen war ich da etwas nachlässig. Mitunter reicht
ein kleinerer Typ doch, wenn man z.B. vorher geeignet skaliert. Am
besten nur die skalaren Typen aus <stdint.h> verwenden, das erleichtert
auch das Folgende.
- logische Operatoren werden auf int-Größe erweitert: Trotz 8-bit Target
ist der AVR-gcc 16-bittig drauf. Für a = ~b wird die eigentliche
Negationsoperation 16-bittig ausgeführt, obwohl beide als uint8_t
definiert sind! Ziemlich blöd, mit einem cast dazwische wird's kleiner:
a = (uint8_t)~b. Dasselbe gilt für & und |, habe ich aber nicht mehr mit
casts ausprobiert.
- Compileroption -mint8 für 8-Bit Arithmetik als Default: Mit obigen
casts überall sähe der Code ziemlich schlimm aus. Blöd auch, wenn man
mal einen Type ändert, dann muß man sorgsam nach den zugehörigen casts
suchen. Mit dem Compilerschalter -mint8 wird das zum Standard. Bei mir
hat das etwa 200 Byte gespart! Man sollte dafür aber keine ints mehr im
Code haben, nur noch Typen definierter Größe aus <stdint.h>.
Literal-Werte muß man ggf. anpassen (z.B. mit postfix L long machen)
damit sie nicht überlaufen, Compiler-Warnings beachten. Ist anscheinend
noch etwas experimentell(?), mit dem aktuellen gcc 4.1.1 geht es nicht,
der kriegt ein Problem mit den 64-bit Typen. Ist aber wohl in Arbeit,
ich habe einen Patch gesehen.
- ein paar Schlagworte habe ich noch, die aber nicht zur Anwendung
kamen: -ffreestanding, soll noch ein pragma für main() geben welches
Prolog/Epilog kappt, vielleicht kann man die Vektortabelle beschneiden.
Wenn ich noch was vergessen habe schreib' ich ein Follow-Up. Fühlt Euch
frei, selbst Tricks und Kommentare anzumerken. ;-)
> statische (globale) Variablen in ein struct sammeln
wusste ich schon - aber das es auf die Reihenfolge ankommt war mir neu!
Danke! :)
Die Unterschiede vom GCC 3.4.6 zum 4.1.1 kommen oft von inline
Funktionen die eigentlich gar nicht geinlined werden sollen ;)
Mit nem prototyp wie:
void xyz(uint16_t param) __attribute__((noinline));
kann man das manuell verhindern.
Spart oft ein paar Bytes - war bei einem Bootloader von mir extrem
ausgeprägt im Vergleich zur alten GCC Version...
Dann noch der Prolog in der Main Funktion - da kommen seit neuestem ja
ne ganze Menge pushs und anderes dazu... mit einem
__attribute__((noreturn)) kann man zumindest ein wenig davon abschalten.
MfG,
Dominik
Dominik s. Herwald wrote:
> Mit nem prototyp wie:> void xyz(uint16_t param) __attribute__((noinline));>> kann man das manuell verhindern.> Spart oft ein paar Bytes ...
Erstmal als "static" deklarieren, vielleicht spart ja dann das Inlining
sogar noch mehr Bytes?
Der rechnet sich nämlich eigentlich aus, wie viel es spart. Sofern er
sich dabei bei nicht-inline-asm-Code verrechnet, darf man dafür auch
gern Bugreports schreiben. Nur bei inline asm ist es sinnlos, da gibt
es keine Methode, dem Compiler mitzuteilen, dass sich darin mehr als
ein Prozessorbefehl versteckt.
Jörg wrote:
> - statische (globale) Variablen in ein struct sammeln: Das> erleichtert dem Compiler die Adressierung, da er den Basiszeiger> wiederverwenden kann. Finde ich aber schwach, daß der Compiler sich> das nicht selbst passend hinlegt.
Wenn du drüber nachdenkst wüsstest du, warum er das nicht kann.
Globale Variablen bekommen erst vom Linker eine Zuweisung der Adresse
verpasst. Damit ist es erst der, der das sortiert. Folglich kann
sich der Compiler nicht einfach die Adresse von irgendeiner Variablen
benutzen und dann einen Offset dazu errechnen, um andere Variablen zu
erreichen.
Toent vielleicht trivial, aber irgendwan kann man bei gleicher Groesse
die Funktionalitaet nicht mehr verdoppeln, wird einen groesseren Chip
benoetigen. Meist gibt es ja Baugleiche. Der Mega8 ist ja eher von der
kleinen Sorte.
Zwischendurch mal danke für die Anmerkungen und den Wiki-Artikel, sowas
schwebte mir auch vor, habe ich hier nur noch nie gemacht, bin recht neu
hier. ;-)
Vielleicht sollte ich mich mal anmelden.
Mittlerweile habe ich noch das Compileflag -mtiny-stack entdeckt, das
hat nochmal ca. 100 Byte gebracht. (Meine Güte, was fange ich nur an mit
all dem freigewordenen Platz... ;-)
Mit 256 Bytes Stack sollte ich auskommen, denke ich. Die meisten meiner
Variablen sind static und modul-global, statische Zustandsvariablen, nur
wenig automatische. Verschachtelungstiefe ist auch nicht so wild, selbst
mit Interrupts. Gibt es eine Möglichkeit, das zu testen?
Ich habe den Header <stdint.h> für -mint8 und gcc 4.1.1 "repariert",
siehe Anhang, damit klappts. Das Flag wird in dem Header zwar schon
korrekt berücksichtigt, aber die 64bit-Typen müssen in dem Fall wohl
raus, die kann der Compiler dann nicht mehr.
__attribute__((noreturn)) für main() habe ich irgendwie nicht
hinbekommen. Der Compiler motzt am Funktionsende, ich hätte ein return,
habe ich aber auskommentiert. Ein for(;;) am Ende von main() scheint als
Hinweis an den Compiler den gleichen Zweck erfüllen, damit wird es
jedenfalls 4 Byte kürzer.
@dl8dtl:
>Wenn du drüber nachdenkst wüsstest du, warum er das nicht kann.>Globale Variablen bekommen erst vom Linker eine Zuweisung>der Adresse verpasst.
Schon, aber das waren keine übergreifend globalen Variablen, sondern
statics, nur für dieses Modul. Da könnte der Compile vielleicht doch
selbst... Naja, ich will mich nicht zu sehr als ahnungslos outen.
@Trog:
Das Design steht fest, es gibt einige Stück davon. Der Code ist auch
praktisch fertig, es kommen keine Features mehr hinzu, nur noch Bugfixes
und sonstige Pflege. Es paßt ja auch gut rein, wie man sieht, wenn man
sich etwas Mühe gibt. Der Mega8 ist für meine Anwendung prima, alles
andere wäre Verschwendung.
vorläufige Zusammenfassung:
Die bisher genanntem Maßnamen haben meinen Code von 6482 auf 5696 Byte
verkleinert, das sind ca. 12%! Ich konnte ein paar Luxusfeatures wieder
einschalten, die ich zuvor rausnehmen mußte um unter 6 KB zu kommen. Den
Code selbst habe ich kaum verändert, nur die Inits und ein ganz paar zu
große Typen. Ist immer noch alles sauber und lesbar, imho. Ich bin
zufrieden und habe was gelernt. :-)
Noch eine Frage zum Schluß: kann man die .lst Files "schöner" bekommen,
möglichst mit eingestreutem C-Code? Mit der neuen global-struct taucht
nur noch diese auf, vorher gaben zumindest die Variablennamen einen
Hinweis, wo man gerade ist. Ich hätte gern mehr Feedback, was der
Compiler im Einzelnen so aus meinen Statements macht.
> logische Operatoren werden auf int-Größe erweitert: Trotz 8-bit Target> ist der AVR-gcc 16-bittig drauf. Für a = ~b wird die eigentliche> Negationsoperation 16-bittig ausgeführt, obwohl beide als uint8_t> definiert sind! Ziemlich blöd, mit einem cast dazwische wird's kleiner:> a = (uint8_t)~b. Dasselbe gilt für & und |, habe ich aber nicht mehr mit> casts ausprobiert.
Habe gerade mal versucht das nachzuvollziehen, konnte jedoch keine
Unterschiede feststellen.
Der GCC 3.4.6 macht in beiden fällen ein:
>In welchem Fall ist das bei dir aufgetreten?
Hab's grad noch mal ausprobiert, bei fast allen Negationen in meinem
Code ist es tatsächlich egal. Bis auf eine, und da kostet es auf einmal
12 Bytes(?):
1
staticvoidset_output(unsignedcharnew_lines)
2
{
3
if(new_lines==global_lines)
4
{
5
return;// nothing to do
6
}
7
8
if(new_lines&(unsignedchar)~global_lines)// new set bit(s)?
9
{
10
// do something
11
}
12
13
// do more stuff
14
}
Allgemein liegt der Hase wohl schon in derartigem Pfeffer, sonst wäre
-mint8 ja nicht so der Bringer bei mir, wo ich alle Typen in der Größe
festgenagelt habe.
Das scheint der Compiler auch nur zu machen, wenn die Verknüpfung in
einer if-Abfrage stattfindet. Aber warum?
Wenn ich das mit einer zusätzlichen Variable mache wird es auch nicht
auf 16-Bit aufgebläht:
Sieht stark danach aus, daß da eine Artanpassungsregel greift: if
erwartet einen Ausdruck vom Typ int, also werden die Operanden, aus
denen der Ausdruck berechnet werden soll, auf int erweitert.
@Jörg Wunsch:
Die Funktionen static zu machen hat nur bei der hier:
1
voidputch(charch)
2
{
3
while(!(UCSRA&(1<<UDRE)));
4
UDR=ch;
5
}
8 Bytes gebracht. Bei den delays brachte das nix.
noinline brachte aber bei der Funktion oben ganze 46 Bytes für den
Bootloader bei -Os.
Ähnlich verhält es sich bei einer einfachen delay Funktion - da ist
sogar genau ein inline assembler Befehl drin - ein nop weil sonst die
ganze delay Schleife wegoptimiert werden würde ;)
Die anderen Funktionen die ein bisschen größer sind, werden für -Os
richtig behandelt und NICHT geinlined. Passiert eigentlich bei mir
bisher nur bei sehr kleinen Funktionen mit zwei drei Zeilen wie die
oben.
Beim alten GCC 3.4.6 brauchte ich das nicht als noinline zu deklarieren
der hat es gleich so gemacht wie vorgesehen.
Eine ähnliche Diskussion war ja schonmal vor kurzem hier:
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=46152
Naja zumindest bevor der der Thread auf Seite 2 etwas Offtopic wurde ;)
MfG,
Dominik
Manchmal ist ein vermeintliches Optimierungsproblem einfach nur ein
Programmierfehler. Denn (~b) ist nicht etwa (b ^ 0xFF) sondern ((b &
0x00FF) ^ 0xFFFF) und hat folglich den Wertebereich 0xFF00-0xFFFF. Der
Vergleich mit (a) wird also stets falsch sein.
> Denn (~b) ist nicht etwa (b ^ 0xFF) sondern> ((b & 0x00FF) ^ 0xFFFF) und hat folglich den Wertebereich> 0xFF00-0xFFFF. Der Vergleich mit (a) wird also stets falsch sein.
Na, das wäre aber böse, dann würde bei mir wohl etliches nicht
funktionieren.
Ich hantiere hier viel mit 8bit-Werten und entsprechenden Masken.
Werd' aber noch in den Assemblercode gucken, das war bisher noch nicht
so mein Ding.
Dominik s. Herwald wrote:
> Ähnlich verhält es sich bei einer einfachen delay Funktion - da ist> sogar genau ein inline assembler Befehl drin - ein nop weil sonst> die ganze delay Schleife wegoptimiert werden würde ;)
Nimm <util/delay.h> dafür.
> Passiert eigentlich bei mir bisher nur bei sehr kleinen Funktionen> mit zwei drei Zeilen wie die oben.
Wie geschrieben, wenn du Fälle hast, wo sich GCC ernsthaft verrechnet
und garantiert kein inline-asm drin ist (auch nicht aus der avr-libc),
dann schreib einen Bugreport für GCC.
Das AVR-Target kann von den GCC-Entwicklern mangels einer geeigneten
Testumgebung nicht getestet werden. Damit kann dort keiner merken,
wenn auf dem AVR eine bestimmte Optimierung sich in das Gegenteil
verkehrt: für die Architekturen, die sie prüfen können, wird nämlich
sehr wohl (automatisiert) geprüft, ob eine bestimmte Optimierung
Nachteile bringt.
Ob da inline-asm-Code drin ist, kannst du im vom Compiler generierten
Assemblercode (das ist nicht der Disassembler-Code!) schnell
feststellen, indem du nach den Kommentaren /* APP */ ... /* NOAPP */
guckst. Den Assemblercode für eine Datei foo.c erhälst du (bei
passend gestaltetem Makefile, z. B. dem von WinAVR) durch »make foo.s«.
> Beim alten GCC 3.4.6 brauchte ich das nicht als noinline zu> deklarieren der hat es gleich so gemacht wie vorgesehen.
Der konnte automatisches Inlining nur bei Optimierung auf
Geschwindigkeit (-O3), nicht bei Optimierung auf Platz -- obwohl es
eben auch durchaus Platzeinsparungen bringen kann.
Jörg wrote:
> Mittlerweile habe ich noch das Compileflag -mtiny-stack entdeckt, ...> Mit 256 Bytes Stack sollte ich auskommen, denke ich. ...
Du hast weniger Stack.
Der Stackpointer wird beim ATmega8 auf 0x45F initialisiert. Da
-mtiny-stack nur SPL manipuliert, ist die niedrigstmögliche Adresse,
die du auf dem Stack nutzen kannst, 0x400. Du hast also nur 96 Bytes
Stack.
Eine ziemlich gefährliche Option, wie ich finde. Du könntest dich
noch rausretten, indem du den Stack woanders hin verlagerst, aber dann
verplemperst du unweigerlich RAM stattdessen.
Ich stimme mit denjenigen hier überein, die bei derartig knapper
Reserve lieber zum nächstgrößeren Controller raten würden, vor allem,
falls spätere Bugfixes ,,im Feld'' notwendig sein könnten. Dann
lieber gleich das ganze Design auf einen ATmega168 hochziehen. Aber
das ist natürlich deine Entscheidung.
> Ich habe den Header <stdint.h> für -mint8 und gcc 4.1.1 "repariert",> siehe Anhang, damit klappts.
Das kannst du gern mal als Bugreport für avr-libc einreichen und dann
dort einen Patch mit reinlegen. Bitte nicht das ganze File, sondern
die Ausgabe von "diff -u" zwischen altem und neuem File. Allerdings
hast du noch einen Fehler drin: Typen wie intmax_t und uintmax_t
sollte es auch bei -mint8 auf jeden Fall geben. Die müssen dann nur
Aliase auf die entsprechenden 32-bit-Typen sein.
-mint8 ist aber ansonsten eine mindestens genauso gefährliche Option
with -mtiny-stack. Alle Funktionen der Standardbibliothek gehen von
16-bit-int aus, und praktisch können sie auch gar nicht anders. Diese
Option hat also nur dann wirklich Sinn, wenn man keinerlei Dinge aus
der Bibliothek benutzt.
> Nimm <util/delay.h> dafür.
ja das wäre eine Idee - werde ich wohl auch mal tun.
Zu dem Verrechnen - da müsste ich mal nen kleinen Beispielcode
zusammenstellen. Aber ob sich das dann noch genauso verhält wie im
großen Programm weiss ich nicht.
Den Code der betreffenden Applikation darf ich leider so nicht
rausgeben...
Aber in der kleinen Funktion da oben:
Beitrag "Re: Tricks, wie ich mein Compilat kleiner kriege"
ist wohl kein inline ASM drin - auch nichts aus der avr-libc -
jedenfalls seh ich da nichts ;)
MfG,
Dominik
Dominik s. Herwald wrote:
> Den Code der betreffenden Applikation darf ich leider so nicht> rausgeben...
Dann kürze ihn soweit ein und anonymisiere das entsprechend, dass
du ihn rausgeben kannst. Wenn du nachweisen kannst, dass das
Inlining überhaupt in einer Konstellation Platz verschwendet trotz
-Os, dann ist es den Bugreport wert.
Hallo Jörg,
hmm das habe ich gerade mal mit einem Minimalbeispiel ausprobiert -
hätte ich zwar nicht gedacht, aber da passierts auch:
1
#include<avr/io.h>
2
3
// void putch(char ch) __attribute__((noinline));
4
5
voidputch(charch)
6
{
7
while(!(UCSRA&(1<<UDRE)));
8
UDR=ch;
9
}
10
11
intmain(void)
12
{
13
putch(0);
14
putch(1);
15
putch(2);
16
putch(3);
17
putch(4);
18
putch(5);
19
putch(6);
20
putch(7);
21
putch(8);
22
putch(9);
23
24
return0;
25
}
Mit -Os (makefile von WinAVR... ) sind das 234 Bytes.
Wenn man in der Zeile
// void putch(char ch) __attribute__((noinline));
die Kommentarzeichen entfernt, sinds nur 216 Bytes!
s. Anhang.
Oder ist das nur bei mir so?
MfG,
Dominik
@Jörg Wunsch:
Vielen Dank für die Anmerkungen zu -mint8 und -mtiny-stack.
-mtiny-stack
Hielt ich bisher für die ungefährlichere Option... Ich wollte auch noch
danach fragen, ob der Stack an einer passenden Grenze steht, bzw. stehen
muß, nun ist die Antwort schon vorher da. ;-)
Wo kommt die 0x45F her, warum so "krumm"? Ich habe in das
ldscripts-Verzeichnis mal oberflächlich reingeschaut und nicht viel
verstanden. RAM habe ich mehr als genug, von daher wäre es kein Nachteil
den Stack umzusetzen. Wie ginge das?
-mint8
Ich sehe da 2 "Issues".
1) Die Standardbibliothek ist bereits kompiliert, in den Funktionen
sollte es intern also keine Probleme geben, sondern "nur" an den
Interfaces; immer dann wenn Typen wie int oder long übergeben werden,
die dann in der Größe anders sind. Daran müßte man es doch erkennen
können? Ich verwende aus den Libs nur EEPROM- und Watchdogfunktionen,
die scheinen in Ordnung zu sein.
2) Das andere Problem ist, daß der Compiler bei Konstanten nur noch
16bittig rechnet, auch wenn sie mit UL Postfix versehen sind. Man müßte
sie auf uint32_t casten. F_CPU ist so ein Beispiel, daraus errechnete
Werte für Baudraten- oder Timerregister gehen schief.
Ich traue mich noch nicht, -mint8 wirklich zu verwenden, müßte zu viel
neu testen.
(Dominiks Beispiel)
Ja, das sieht stichhaltig aus. Funktioniert auch noch, wenn man die
Funktion static macht.
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=31528
Kannst dich ja als Cc drauf setzen, wenn du möchtest (oder mir deine
email-Adresse mailen, dann setze ich dich drauf).
Jörg wrote:
> Wo kommt die 0x45F her, warum so "krumm"?
Es ist das Ende des RAMs auf deinem ATmega8.
Andere AVRs haben andere RAM-Aufteilung, da könntest du mehr Glück
haben.
> RAM habe ich mehr als genug, von daher wäre es kein Nachteil> den Stack umzusetzen. Wie ginge das?
Es müsste genügen, auf der Linker-Kommandozeile den Wert des Symbols
__stack zu setzen. Das kann man mit der Option --defsym, der man bei
der Übergabe durch den Compilertreiber noch ein -Wl, voranstellen
muss. Dabei den Offset 0x800000 für den RAM nicht vergessen, also
zum Beispiel
1
-Wl,--defsym=__stack=0x8003ff
Der RAM oberhalb 0x400 (bis 0x45f) ist damit praktisch erst einmal
nicht mehr benutzbar.
> 2) Das andere Problem ist, daß der Compiler bei Konstanten nur noch> 16bittig rechnet, auch wenn sie mit UL Postfix versehen sind. Man> müßte sie auf uint32_t casten.
ULL benutzen.
Dominik s. Herwald wrote:
> Extra anmelden wollte ich mich wegen der "Kleinigkeit" eigentlich nicht.
Hmpf. Man kann da nur die email-Adressen von Leuten eintragen, die
angemeldet sind.
Aber anmelden kost nüscht, und du erwirbst damit das Recht, eigene
Kommentare zu anderen Bugreports hinzuzufügen. ;-)
Dominik s. Herwald wrote:
> Habe mich mal angemeldet.
Ja, hat mir der Bugzilla schon gepetzt. ;-)
Da sich das sogar auf i386 nachweisen lässt, haben wir vielleicht
gar nicht so schlechte Karten, dass sich da einer drum kümmert.