Hallo zusammen,
ich habe da als Programmierer einen Gewissenskonflikt. Doch zunächst
will ich mal die Situation schildern.
Ich habe einen Bootloader geschrieben, um einen ATmega328 mittels
W5100-Ethernet-Controller über TCP/IP zu flashen. Ziel war es, dass der
Bootloader nicht mehr als 2KB groß wird.
Also habe ich mich im Internet um Quellen zum W5100 bemüht, diese
übernommen, stark abgewandelt und möglichst noch optimiert. Soweit so
gut.
Herausgekommen ist:
w5100.c - Chip-spezifische Funktionen (static) plus Interface-Funktionen
wie ip_init(), tcp_listen(), tcp_recv() usw.
w5100.h - Deklaration der Interface-Funktionen
ipbootloader.c - Der eigentliche Bootloader
Das alles zusammengelinkt erzeugt ein Programm von 1994 Bytes. Ich bin
also knapp unter der Grenze von 2KB, habe aber keine Spielräume bzgl.
Erweiterungen.
Ich dachte mir: Was passiert eigentlich, wenn ich die
Interface-Funktionen auch static deklariere und in ipbootloader.c nicht
w5100.h per include einfüge, sondern w5100.c?
Gemacht, getan: Die Programmgröße reduzierte sich auf 1570 Bytes. Ist ja
auch klar, denn nun kann der Compiler die Interface-Funktionen (da nun
static) auch inlinen. Aber dass der Unterschied so groß ist?
Ich bin eigentlich ein Freund des modularen Programmierens, aber die
Ersparnis war so enorm, dass ich w5100.c als optionale "Include-Lib"
umgebaut habe.
Grob umrissen:
ipbootloader.c:
1
#define W5100_INCLUDE_LIB
2
#include"w5100.c"
ws5100.c:
1
#ifdef W5100_INCLUDE_LIB
2
#define STATIC static
3
#else
4
#define STATIC
5
#endif
6
7
...
8
9
STATICuint8_trecv(....)
10
{
11
...
12
}
Mit diesem Konstrukt kann man w5100.c einmal klassisch zum Hauptprogramm
dazu linken oder halt einfach "includen". Im ersten Fall steht dann wie
gewohnt:
1
#include"w5100.h"
Im zweiten Fall:
1
#define W5100_INCLUDE_LIB
2
#include"w5100.c"
... und man kann sich über das kleinere (und wohl auch performantere)
Programm freuen.
Ich weiß aber, dass ein paar Hartgesottene hier prinzipielle Regeln
haben, nämlich u.a.:
- "Includiere niemals eine C-Datei, immer nur H-Datei"
- "Definiere niemals in H-Dateien Funktionen"
Daran halte ich mich normalerweise auch. Aber muss man sich deshalb auf
einem Mikrocontroller mit schlechter Programmgröße und ineffektiverem
Code zufriedengeben?
Kann es sein, dass in w5100.c nicht benutzte Funktionen enthalten sind,
die zusätzlichen Flash-Speicher belegen? Deklarierst du sie als static
und includest sie direkt, werden sie vom Compiler wegoptimiert.
Mit den entsprechenden Optionen (die ich gerade nicht im Kopf habe)
kannst du den Compiler bzw. den Linker anweisen, jede Funktion in ein
eigenes Segment zu schreiben und unbenutzte Segmente wegzulassen. Dann
sollten auch bei modularer Programmierung unbenutzte Funktionen
wegoptimiert werden.
Frank M. schrieb:> Aber muss man sich deshalb auf einem Mikrocontroller mit schlechter> Programmgröße und ineffektiverem Code zufriedengeben?
Man kann auch einen aktuellen GCC verwenden und mit "-flto" kompilen &
linken, dann hat man die gleichen Vorteile auch wenn man seperate .c(pp)
Dateien verwendet.
Yalu X. schrieb:> Kann es sein, dass in w5100.c nicht benutzte Funktionen enthalten sind,> die zusätzlichen Flash-Speicher belegen? Deklarierst du sie als static> und includest sie direkt, werden sie vom Compiler wegoptimiert.
Tatsächlich ist w5100.c möglichst allgemein gehalten, da ich sie als Lib
weiterverwenden will. Sie enthält tatsächlich auch noch Funktionen für
UDP, die für den Bootloader nicht benötigt werden. Ich habe daher den
eigentlichen Source über eine w5100config.h konfigurierbar gemacht, so
dass mit
1
#define W5100_NEED_UDP 0
diese Funktionen ausgeblendet werden.
Das funktioniert auch perfekt, denn sonst würde der gcc meckern und
mitteilen, dass eine static-Funktion nicht benutzt wird. Den Gegencheck
habe ich mit
1
#define W5100_NEED_UDP 1
auch schon gemacht (gcc trällert los), denn das war auch meine erste
Idee. ;-)
Es sieht tatsächlich so aus, als ob das Inlinen wirklich unheimlich viel
bringt. Und es wäre dumm, das nicht auszunutzen.
Yalu X. schrieb:> Mit den entsprechenden Optionen (die ich gerade nicht im Kopf habe)> kannst du den Compiler bzw. den Linker anweisen, jede Funktion in ein> eigenes Segment zu schreiben und unbenutzte Segmente wegzulassen.
Kompilieren+linken mit -ffunction-sections -fdata-sections und linken
mit -Wl,--gc-sections , grad gant vergessen das noch mit zu posten
Dr. Sommer schrieb:> Man kann auch einen aktuellen GCC verwenden und mit "-flto" kompilen &> linken, dann hat man die gleichen Vorteile auch wenn man seperate .c(pp)> Dateien verwendet.
Das wäre eine Alternative. Ich verwende den gcc 4.7.2, der sollte
halbwegs aktuell sein. Ich werde das mal ausprobieren, auch wenn sich
bei mit bei Verwendung von gcc-spezifischen Optionen immer die
Nackenhaare aufstellen.
Ich bin ein defensiv eingestellter Programmierer und hab es gern immer
möglichst "kompatibel". Die Verwendung als "Include-Lib" ist nicht
abhängig von der gcc-Version.
Die andere Verwendung auch nicht.
Codegröße ist immer compilerabhängig. Die zählt also nicht für einen
maximal defensiven, portabel programmierenden Entwickler wie dich.
Dr. Sommer schrieb:> Man kann auch einen aktuellen GCC verwenden und mit "-flto" kompilen &> linken, dann hat man die gleichen Vorteile auch wenn man seperate .c(pp)> Dateien verwendet.
Grundsätzlich finde ich LTO sehr praktisch. ALlerdings habe ich mir
damit auch schonmal ins Knie geschossen, deswegen bin ich da vorsichtig
mittlerweile.
Für den TO dürfte die Lösung die du weiter unten ja auch genannt hast
zielführender sein, also -ffunction-sections und -fdata-sections
verwenden.
Dr. Sommer schrieb:> Yalu X. schrieb:>> Mit den entsprechenden Optionen (die ich gerade nicht im Kopf habe)>> kannst du den Compiler bzw. den Linker anweisen, jede Funktion in ein>> eigenes Segment zu schreiben und unbenutzte Segmente wegzulassen.> Kompilieren+linken mit -ffunction-sections -fdata-sections und linken> mit -Wl,--gc-sections , grad gant vergessen das noch mit zu posten
Danke, aber ich bin mir sicher, dass es keine unbenutzten Funktionen
gibt. Ich achte immer penibel auf Warnungen. Wird eine static-Funktion
nicht verwendet, sagt der gcc das ja auch.
Also daran liegt es nicht. Ich suche auch nicht nach Schwachstellen im
Code (ich werde diesen sowieso hier in einem gesonderten Artikel
veröffentlichen), sondern nach einer mindestens genauso guten Methode,
modular zu programmieren und trotzdem dieselbe Programmgröße zu
erreichen wie bei einer Include-Lib.
Masl schrieb:> Für den TO dürfte die Lösung die du weiter unten ja auch genannt hast> zielführender sein, also -ffunction-sections und -fdata-sections> verwenden.
Nein. Damit blende ich lediglich unbenutzte Funktionen aus. Die habe ich
aber gar nicht.
Frank M. schrieb:> auch wenn sich bei mit bei Verwendung von gcc-spezifischen Optionen> immer die Nackenhaare aufstellen.
Wie viele andere AVR C-Compiler gibt es denn noch? Für die musst du dann
halt deren entsprechende Optionen verwenden.
Frank M. schrieb:> Die Verwendung als "Include-Lib" ist nicht abhängig von der gcc-Version.
Wer sich weigert neue Software zu installieren ist halt selber schuld
dann und muss mit einem größeren Programm leben - ist ja nicht so dass
es nicht funktioniert. Neuere Versionen können vermutlich auch die
"normalen" Optimierungen -O2 etc besser. Lieber eine nicht-antike
Compilerversion verlangen als hässlichen Code zu schreiben...
Gerade getetstet:
-flto als Compiler- und Linker-Option:
Ergebnis: Größe = 3134 Bytes, also nochmal rund 1k größer und nicht
kleiner.
Dann noch getestet:
Zusätzlich zu "-flto" noch Compiler- und Linker-Optionen:
-ffunction-sections
-fdata-sections
und als Linker-Option:
-Wl,--gc-sections
Ergebnis: Größe = 3134 Bytes, also unverändert zu oben und absolut
suboptimal.
hier der Output:
Dr. Sommer schrieb:> Wer sich weigert neue Software zu installieren ist halt selber schuld> dann und muss mit einem größeren Programm leben - ist ja nicht so dass> es nicht funktioniert. Neuere Versionen können vermutlich auch die> "normalen" Optimierungen -O2 etc besser. Lieber eine nicht-antike> Compilerversion verlangen als hässlichen Code zu schreiben...
gcc4.7.2 ist antik?!? Das glaubst Du doch selbst nicht.
Frank M. schrieb:> Ich weiß aber, dass ein paar Hartgesottene hier prinzipielle Regeln> haben, nämlich u.a.:>> - "Includiere niemals eine C-Datei, immer nur H-Datei"> - "Definiere niemals in H-Dateien Funktionen"
Die einzig wahre Pauschalaussage ist, daß Pauschalaussagen immer
irgendwo falsch sind.
Man darfselbstverständlich alles, wenn es sinnvoll ist, und man weiß was
man tut. Bei dir ist der Ansatz sinvoll, und du weisst, was du tust.
Alles in Ordnung.
Oliver
Frank M. schrieb:> Danke, aber ich bin mir sicher, dass es keine unbenutzten Funktionen> gibt. Ich achte immer penibel auf Warnungen. Wird eine static-Funktion> nicht verwendet, sagt der gcc das ja auch.
Bei den vorgeschlagenen Compiler-Schalter geht es nicht um
static-Funktionen. Werden solche nicht verwendet landen sie eh nicht im
Code, da der Compiler, durch das static, sichergehen kann dass diese
auch nicht von extern aufgerufen werden sollen.
Die Compiler-Schalter sorgen eher dafür, dass deine
Interface-Funktionen, also alles was nicht static ist, bei Nichtgebrauch
wegoptimiert werden können.
(Genauer: sie werden dann nicht dazu gelinkt. Die kleinste Einheit mit
der der Linker Arbeiten kann sind Sektionen. Standardmäsig entspricht
ein c-File einer Sektion. Somit werden entweder alle oder garkeine
Funktionen eines c-Files gelinkt. Legst du aber alle Funktionen in
eigene Sektionen kann der Linker unbenutzte Interfaces einfach
weglassen.)
Frank M. schrieb:> hier der Output:
Sorry, das war der Lauf mit lediglich "-flto". Hier der finale mit allen
Optionen, der aber keine Besserung bringt:
Frank M. schrieb:> -flto als Compiler- und Linker-Option:>> Ergebnis: Größe = 3134 Bytes, also nochmal rund 1k größer und nicht> kleiner.>> Dann noch getestet:>> Zusätzlich zu "-flto" noch Compiler- und Linker-Optionen:>> -ffunction-sections> -fdata-sections
Bei Verwendung von LTO musst du dem Linker den Optimierungsschalter
mitgeben (also -Os oder -O3).
Bei LTO ist nämlich der Linker für das Optimieren des Codes
verantwortlich. Fehlt dort das Flag ist das Resultat, wie von dir
beobachtet, sogar schlechter.
Masl schrieb:> Die Compiler-Schalter sorgen eher dafür, dass deine> Interface-Funktionen, also alles was nicht static ist, bei Nichtgebrauch> wegoptimiert werden können.
Das weiß ich doch! Aber da ich alle Interface-Funktionen mit dem Makro
STATIC im Falle der Include-Lib ja definitiv static mache, bekomme ich
das direkt mit, wenn eine (static) Funktion unbenutzt bleibt.
Verstanden?
Masl schrieb:> Bei Verwendung von LTO musst du dem Linker den Optimierungsschalter> mitgeben (also -Os oder -O3).
-Os ist bei mir selbstverständlich, siehe geposteten Output.
Leute, behandelt mich bitte nicht als Anfänger. ;-)
Frank M. schrieb:> Das weiß ich doch! Aber da ich alle Interface-Funktionen mit dem Makro> STATIC im Falle der Include-Lib ja definitiv static mache, bekomme ich> das direkt mit, wenn eine (static) Funktion unbenutzt bleibt.>> Verstanden?
Achso meinst du das. Ja ok, macht Sinn.
Dann kann man nur annehmen dass der Compiler dadurch aggresiver
Optimieren kann als im Falle der "echten" Interface-Funktionen.
Dann müsste LTO ein mindestens gleich-gutes, wahrscheinlich besseres
Ergebnis bringen.
Masl schrieb:> Frank M. schrieb:>> -Os ist bei mir selbstverständlich, siehe geposteten Output.>> Aber nur beim Compileraufruf, nicht beim Linkeraufruf ;-)
Wow! Das hats gebracht! Mir war noch nie bewusst, dass -Os auch vom
Linker ausgewertet werden könnte!
Tatsächlich kommt dann exakt dasselbe Ergebnis wie bei Verwendung von
Include-Libs heraus:
1
AVR Memory Usage
2
----------------
3
Device: atmega328p
4
5
Program: 1570 bytes (4.8% Full)
6
(.text + .data + .bootloader)
7
8
Data: 26 bytes (1.3% Full)
9
(.data + .bss + .noinit)
Ich bedanke mich bei allen für ihre Beteiligung. Ich werde zukünftig
-flto und -Os als Compiler- und Linkeroption verwenden.
Ich habe lediglich noch eine Frage an Dich:
Wie kam der Knieschuss mit -flto zustande? Möchte da ja nur ungern in
dieselbe Falle tappen.
Frank M. schrieb:> Ich bedanke mich bei allen für ihre Beteiligung. Ich werde zukünftig> -flto und -Os als Compiler- und Linkeroption verwenden.>> ...>> Wie kam der Knieschuss mit -flto zustande? Möchte da ja nur ungern in> dieselbe Falle tappen.
LTO ist keine Optimierung des Linkers, es wird lediglich zur Linkzeit
nochmals der Compiler aufgerufen, und zwar mit allen Objekten. Die
Optionen werden jedoch über die Kommandozeile übergeben.
Die mit LTO übersetzten Objekte enthalten i.W. den AST (abstract syntax
tree) zu einem frühen zeitpunkt des Compilevorgangs, also vor Inlining
von Funktionen und anderen Optimierungen. Beim LTO-Lauf extrahiert der
Compiler die LTO-Info und hat dann die globale Information über alle
Module /
Funktionen. Der Assembler-Code in den .text-Sections ist dann Makulatur;
der Compiler interessiert sich nicht dafür.
Um die Aufrufe zu sehen, kann man z.B. -v -Wl,-v angeben.
Das Ergebnis von LTO sollte ähnlich zu dem sein, wenn man alle C-Quellen
beim Compilieren angibt, denn auch dann hat der Compiler die globale
Sicht, d.h. sowas wie
gcc a.c b.c c.c -o <optionen> abc.elf
Johann L. schrieb:> Die mit LTO übersetzten Objekte enthalten i.W. den AST (abstract syntax> tree) zu einem frühen zeitpunkt des Compilevorgangs, also vor Inlining> von Funktionen und anderen Optimierungen. Beim LTO-Lauf extrahiert der> Compiler die LTO-Info und hat dann die globale Information über alle> Module /
Danke für die Hintergrundinfos. Für den Laien kann man also sagen: Der
gcc schmeisst alle Module in einen Topf und betrachtet sie wie eine
große C-Datei.
Aber leider beantwortet das noch nicht die Frage nach dem "Knieschuss".
Masl schrieb oben, dass er sich mit -flto mal heftig ins Knie geschossen
hat.
Ich würde einfach gern die Gefahr kennen ;-)
Mein Knieschuss, hehe :-)
Ich wollte Funktionen für den Zugriff auf Portpins anlegen, ähnlich den
digitalWrite aus den Arduino Libs. Ich habe dabei mit sehr vielen
Compiler- und Linker Flags herumgespielt, da die Arduino-Lösung ja
bekanntlich sehr unperformant ist.
Hauptproblem ist ja, der Compiler kann schlecht optimieren. Auch kann er
nicht ohne weiteres sbi- und cbi - Befehle erzeugen.
Gegeben:
Dio.h
Wie zu erwarten war wird hässlicher, aufgeblähter Code erzeugt. Anstatt
einen Takt braucht das Bit-Setzen ein Vielfaches.
Dann das Ganze mit LTO probiert und den Assemblercode begutachtet.
Wow, der Funktionsaufruf in main resultiert tatsächlich in einem
einzigen (!) sbi - Befehl!
Ich hab noch einige Tests gemacht und verglichen, auch mit inline und
always-inline (da gibts auch Knieschüsse ;-)) und war mit dem LTO
Ergebnis sehr zufrieden. Konnte keinen Fall nachstellen wo es nicht
klappt.
...bis zur Praxis :-)
Gegeben sei beispielsweise ein Modul was ein Schieberegister per
Pinwackeln anspricht.
Um das Modul universell zu halten wurde ein leicht objektorientierter
Ansatz gewählt in dem jedes Schieberegister durch eine Konfiguration
beschrieben wird:
1
typedefstruct
2
{
3
Dio_channel_tenablePin;
4
Dio_channel_tclockPin;
5
Dio_channel_tdataPin;
6
uint8_tsize;
7
}
8
latchConfig_t;
main.c
1
externlatchConfig_tmyConfig[NUMBER_OF_LATCHES];
2
...
3
latch_write(&myConfig[1],0xA5);
Und da schepperts dann. Durch das Ablegen der Channelnummern in einer
Variable, die auch noch in einem anderen c-File liegt (extern) und nicht
const ist hab ich dem Compiler wieder jegliche Möglichkeit zur
Optimierung genommen. Da hilft auch kein LTO, das kann er nicht auf ein
sbi runterbrechen. Das erzeugt in jedem Fall mehrere Aufrufe zu meiner
Dio_WriteChannel Funktion. Und das will man nicht haben.
Deswegen sollte man bei aller Euphorie nicht vergessen was man macht und
wo die Grenzen liegen :-)
PS: ich hab leider immer noch keine Performante Lösung für den AVR
gefunden mit dem ich meine Kanalnummern irgendwo wegspeichern kann.
PPS: nein Peter, sbit.h kann dies auch nicht :-)
Masl schrieb:> Und da schepperts dann. Durch das Ablegen der Channelnummern in einer> Variable, die auch noch in einem anderen c-File liegt (extern) und nicht> const ist hab ich dem Compiler wieder jegliche Möglichkeit zur> Optimierung genommen.
Danke für die ausführliche Beschreibung, werde Dein Posting aber
bestimmt noch 2- bis 3-mal lesen müssen, bis ich das eigentliche Problem
bis ins Detail verstanden habe. Wahrscheinlich habe ich es erst dann
kapiert, wenn ich mal selbst in so eine Situation komme. ;-)
Oliver S. schrieb:> Man darfselbstverständlich alles, wenn es sinnvoll ist, und man weiß was> man tut. Bei dir ist der Ansatz sinvoll, und du weisst, was du tust.> Alles in Ordnung.
Ich danke auch Dir für Deinen Zuspruch, ich werde da zukünftig bei
anderen Problemen daran denken.
Aber konkret hier hat die Option -flto dann doch den größeren Charme ;-)
Frank M. schrieb:> Danke für die ausführliche Beschreibung, werde Dein Posting aber> bestimmt noch 2- bis 3-mal lesen müssen, bis ich das eigentliche Problem> bis ins Detail verstanden habe. Wahrscheinlich habe ich es erst dann> kapiert, wenn ich mal selbst in so eine Situation komme. ;-)
Sein Problem war:
Er hat angenommen, dass er ein wunderbares System gefunden hat, dass ihm
spätestens bim Linken alles wieder ins Lot rückt und die Portzugriffe
auf die Low-Level Bitbefehle runteroptimiert.
Er hat aber übersehen, dass dieses in seinem Fall prinzipiell nicht
möglich ist.
Karl Heinz schrieb:> Er hat angenommen, dass er ein wunderbares System gefunden hat, dass ihm> spätestens bim Linken alles wieder ins Lot rückt und die Portzugriffe> auf die Low-Level Bitbefehle runteroptimiert.> Er hat aber übersehen, dass dieses in seinem Fall prinzipiell nicht> möglich ist.
Genau das "Prinzip", warum es nicht klappen kann, war mir nach dem
ersten Lesedurchgang nicht klar. Mittlerweile jedoch schon.
Danke.
Karl Heinz schrieb:> Sein Problem war:>> Er hat angenommen, dass er ein wunderbares System gefunden hat, dass ihm> spätestens bim Linken alles wieder ins Lot rückt und die Portzugriffe> auf die Low-Level Bitbefehle runteroptimiert.> Er hat aber übersehen, dass dieses in seinem Fall prinzipiell nicht> möglich ist.
Hab auch mit const usw. noch rumgespielt und damit zumindest obiges
Problem für genau diesen Fall lösen können.
Allerdings war mir das Ganze dann zu unsicher und ich habs bleiben
lassen.
Johann L. schrieb:> LTO ist keine Optimierung des Linkers, es wird lediglich zur Linkzeit> nochmals der Compiler aufgerufen, und zwar mit allen Objekten. Die> Optionen werden jedoch über die Kommandozeile übergeben.
Jetzt bin ich doch nochmal ins Grübeln gekommen...
Ich habe verstanden, dass der Linker schlussendlich den gcc nochmal
aufruft über alle C-Module, damit das mit dem LTO auch klappen kann.
Aber woher kennt der gcc zu diesem Zeitpunkt die vormals benutzten
Compiler-Optionen wie zum Beispiel:
... -gdwarf-2 -std=gnu99 -DF_CPU=16000000UL ....
Die muss der gcc beim ersten Aufruf ja irgendwo mitgespeichert haben,
damit der durch den Linker aufgerufene "Rundumschlag-gcc" sie am Ende
auch verwenden kann?
Nein, gespeichert wird da nix.
Aus technischer Sicht gibt es ein neues Frontend, also praktisch eine
neue Sprache (lto). C-Programme werden vom cc1 übersetzt, C++ Programme
von cc1plus, Assembler-Programme von as und lto-Programme eben von lto1.
lto wird produziert von cc1 bzw. cc1plus, die den bereits
präprozessierten Code erhalten. lto ist quasi eine Byte-Code, der nach
dem Präprozessing und nach dem C/C++ Parsing erstellt wird.
Insbesondere braucht lto1 nicht mehr die Syntax der Eingabe zu checken;
das ist alles schon passiert.
Johann L. schrieb:> lto wird produziert von cc1 bzw. cc1plus, die den bereits> präprozessierten Code erhalten. lto ist quasi eine Byte-Code, der /nach/> dem Präprozessing und nach dem C/C++ Parsing erstellt wird.> Insbesondere braucht lto1 nicht mehr die Syntax der Eingabe zu checken;> das ist alles schon passiert.
Ah! Jetzt hab ich es komplett verstanden. Wirklich pfiffig.
Danke.