Hallo
Ich benutze beruflich wie auch privat den CodeVision C-Compiler.
Prozesoren: ATtiny26, ATMega32, ATMega128.
Nun möchte ich auf Eclipse mit WinAVR/AVR-GCC als Compiler umsteigen.
Dabei interessiert mich hauptsächlich die bessere IDE von Eclipse und
die möglichkeiten Plugins einzubinden wie z.B. CVS oder ähnliches.
Zu Testzwecken habe ich ein bestehendes Project, welches mit CV
geschrieben wurde auf WINAVR angepasst (Änderung von Interruptaufrufe,
Includes, Bitvarialen, Flashkonstanten, EEPromzugriffe).
Nach dem Compilieren mußte ich aber feststellen, das der Code um ~18%
größer geworden ist.
CodeVision, Ooptimierung max.Size, 1805 Bytes
WINAVR mit -Os, 2132 Bytes
Auch die Optionen -O1 bis -O3 (ohne -Os) haben keine Verringerung der
Codegröße gebracht.
Da das Projekt auf einem Tiny26 mit 2k Flash laufen soll, hätte ich
schon ein Problem.
Im privaten Sector würde ich einfach einen größeren Prozessor nehmen.
Aber im beruflichen Sector, wo auch Stückzahlen eine Rolle spielen,
erzeugt jeder Cent der für einen größeren Prozessor ausgegeben werden
muß entsprechende Mehrkosten.
Ist der Compiler beim optimieren einfach schlechter oder warum kommt es
zu diesem Ergebnis?
Gibt es weitere Compiler die mit Eclipse zusammen arbeiten, aber besser
optimieren?
Gruß Steffen
Steffen Graap wrote:
> Ist der Compiler beim optimieren einfach schlechter oder warum kommt es> zu diesem Ergebnis?
Kann man, ohne den Code zu sehen, nichts dazu sagen.
Kannst du mal die Codegrößen der einzelnen Funktionen von Codevision
aufschreiben? Die größte Funktion ist INT0_vect, aber wenn ich mir
den generierten Assemblercode ansehe, finde ich da kaum noch
Optimierungspotenzial. Gerade in Bereichen wie dem Aufblähen von
8-bit-Zahlen auf 16 bits (integer promotion) ist der GCC hier besser,
als ich erwartet hätte (das ist sonst seine schwache Seite).
Wenn man es mit -mint8 compiliert, passt es gerade so in die 2 KiB
rein. Das könnte für eine derartige Applikation sogar legal sein,
aber schick ist es nicht. Mich würde mal interessieren, wo CV hier
so viel besser ist, dann guck ich nochmal weiter.
Achte bitte auf eine einheitliche Groß-/Kleinschreibung. Wenn die
Datei boot.h heißt, sollte dann nicht #include "Boot.h" stehen. Zum
Glück war's nur eine Stelle.
Es könnte hilfreich sein, in der ISR einige der "static" Vars vorneweg
zu laden und hinten wieder wegzuspeichern. Ziemlich viel Platz wird mit
den vielen LDS/STS verbraten, Register sind billiger.
Oder: Die statischen und externen Variablen alle miteinander in eine
struct packen und per Pointer adressieren. LDD ist nämlich kürzer als
LDS.
Im Anhang habe ich mal das Projekt in CodeVision-Style angehängt. In der
bm8.lst kann man gut die Zuornung von assembler zu C-Code sehen.
Die Größe der Funktion ist INT0_vect in den beiden Compiler wie folgt
CV: 410 Byte (von 0x131 - 0x2CB)
WINAVR: 634 (608) laut *.s Datei
Ich denke eine wichtige Sache ist, das in CV Bitvariablen, soweit wie
möglich (bis 104 Bit), in die Prozessorregister gelegt werden. Das hat
den Vorteil, das man mit SET/CLT & BLD ein Bit setzen/löschen kann
(3Byte) und mit SBIC/SBIS die Abfrage machen kann ohne vorhergehenden
Ladebefehl.
Das zweite was ich gefunden habe ist so etwas:
lds r24,u8Adresse
cpi r24,lo8(4)
brsh .L57
lds r24,u8Adresse (nicht nötig, kostet 4 Byte)
clr r25
subi r24,lo8(-(au8Prog))
sbci r25,hi8(-(au8Prog))
Die option -mint8 ist ein guter Vorschlag. Ich hatte mich schon
gewundert warum bei 8bit Variablen immer mit 16bit gerechnet wurde.
@ Andreas Kaiser
Danke für deine Vorschläge, werde sie berücksichtigen
Gruß Steffen
sorry
das Laden der Variablen in Register funktioniert aber nur in einer
Interrupt-Routine. Hier wird die Abarbeitung ja nicht durch andere
Interrupts unterbrochen, sodas die Register überschriben werden könnten.
Welche Register sollte man für soetwas benutzen? Hat der Compiler
bevorzugte Register?
Sollte ein Compiler so etwas nicht von alleine machen (Optimierung)?
Gruß Steffen
Richtig, es ging mir nur um die ISR. Aber das ist der mit Abstand
grösste Brocken.
Er sollte es alleine machen, ja, aber manchmal hilft es, dem Compiler
entgegenzukommen. Ausprobieren. Mit Registern meinte ich eher, Variablen
die von der Arbeitsweise her "static" sein müssen, vorneweg in "auto"
Variablen zu laden, nicht in explizite Register.
Wäre es nicht ausgerechnet ein schon angestaubter Tiny26, liessen sich
auch die bei neueren AVRs vorhandenen 3 GIOR Bytes im I/O-Space
platzsparend einsetzen. Atmel dürfte die als Analogon zum Bitspace der
8051 verstanden haben, für überwiegend bitadressierten Kram sind Bytes
im I/O-Bereich ideal.
Was die Optimierung von in Speicher plazierten Variablen sehr effektiv
aushebeln kann ist, ist die Gefahr von Aliasing. Vielleicht ist etwas
gewonnen, wenn du GCC davon überzeugen kann, dass diesbezüglich von
pu8Bytezeiger keine Gefahr ausgeht.
Grund für den bei CV erheblich kürzeren Code ist eine
Optimierungstechnik, die im GCC mangels Sinn (Speed) auf
Workstations/Servern nicht enthalten ist: Auslagerung kurzer
Codesequenzen in kleinste Unterprogramme (bm8.lst: OPTIMIZER ADDED
SUBROUTINE).
Im Zusammenhang mit GCC ist mir dieser Schritt beim Microchip C30
begegnet (Procedural Abstraction), dort aber als proprietäre Erweiterung
in einem separaten Compiler-Pass realisiert.
Beispiel:
...
variable += 1; // 5 Worte
...
variable += 1;
...
variable += 1;
...
variable += 1;
...
ist kürzer, wenn realisert als
void incr(void) { variable += 1; }
...
incr(); // 1 Wort
...
incr();
...
incr();
...
incr();
...
Steffen Graap wrote:
> Das zweite was ich gefunden habe ist so etwas:>> lds r24,u8Adresse> cpi r24,lo8(4)> brsh .L57> lds r24,u8Adresse (nicht nötig, kostet 4 Byte)> clr r25> subi r24,lo8(-(au8Prog))> sbci r25,hi8(-(au8Prog))
Das hast du ihm aber extra so gesagt, indem du einen ganzen Haufen
Variablen als volatile deklariert hast. Damit zwingst du den
Compiler, sie immer aus dem Speicher zu lesen und sie jedesmal
wieder zurück zu schreiben.
Ich überblicke die ganze state machine da nicht, aber ich vermute
mal ganz stark, dass keine einzige davon volatile sein muss. Wenn
überhaupt, dann würde wahrscheinlich ein Synchronisationsflag als
volatile genügen. Leider ist das alles recht unübersichtlich, wenn
man nur mal so draufguckt.
Ich sehe gerade, dass der CV einige der Variablen in globale Register
gepackt hat. Das kann er, da er ja immer alle Dateien zusammen als
eine einzige C-Datei compiliert und daher nie von externen Moduln
ausgehen muss. Das könnte man dem GCC natürlich auch noch nahelegen.
Allerdings sind deine Headerdateien ziemlich durcheinander und nicht
sauber nach Definition und Deklaration getrennt, daher gelingt mir
das jetzt nicht auf Anhieb.
Deine EEPROM-Zugriffe stimmen für den GCC übrigens so noch nicht,
da musst du explizit eeprom_write_...() aufrufen.
Yep, die volatiles hatte ich glatt übersehen. Aber da passt der Trick
mit dem pointer-to-struct rein. Denn damit lässt sich diese
optimierungsunfreundliche Eigenschaft recht elegant innerhalb der ISR
umgehen, und da ist das ja ohnehin nicht relevant. D.h. wenn der Pointer
auf die Daten nicht volatile ist, kann optimiert werden. Und ausserhalb
der ISR ist es weiterhin volatile.
Das die ganzen Variablen als volatile deklariert sind, hat den Grund,
das im Tutorial für WINAVR steht, das Variablen die innerhalb und
außerhalb von ISR benutzt werden, so deklariert werden sollten. In CV
brauchte ich dies nicht machen.
Mit der Statemachine wird einfach ein Bus (Selectrix) ausgewertet, mit
jedem Bustakt wird auf das nächste Bit geschaltet.
Das GCC mit den Prozessorregistern recht sparsam umgeht ist mir auch
schon aufgefellen. In CV wird R0 und R1 für die Assemblerbefehler, die
Register R2 - R15 für Bitvariabeln (Anzahl einstellbar), alle übrigen
(außer X, Y und Z, für die globalen Variablen (automatisch oder manuel
zugewiesen) genutzt.
Das mit den EEPromzugriffen ist mir bekannt, habe ich aber der
Einfachheit halber weg gelassen, da der Compiler nicht gemeckert hat.
Dies würde den Code aber nochals aufblähen.
Gruß Steffen
Steffen Graap wrote:
> In CV brauchte ich dies nicht machen.
Mag sein, aber GCC ist nicht für Microcontroller optimiert, sondern für
High-End Maschinen in denen zeilen/blockübergreifende Optimierung
wichtig ist. Und es ist auch nicht GCC oder Jörg, der "volatile"
fordert, sondern der C Standard seit 20 Jahren.
> Das GCC mit den Prozessorregistern recht sparsam umgeht
Er hat bei "volatile" einfach keine Chance, denn das ist letztlich
nichts anderes also die Aussage "führe bitte keine Registeroptimierung
durch". Er tut damit nur was du ihm sagst.
Also:
volatile struct S {
int var1;
int var2;
} globale_variablen;
und dann in der ISR:
struct S *gvars = (struct S *) &globale_variablen;
... gvars->var ...
und der Spuk dürfte vorbei sein. In der ISR selbst besteht für
"volatile" kein Grund, solange du auf nested interrupts verzichtest.
Naja, ist ziemlich unsauber. Ich würde eher dafür plädieren, volatile
nur dort zu benutzen, wo es wirklich notwendig ist. Allerdings ist
diese Abwägung zuweilen ziemlich schwierig. Normalerweise sollte es
komplett genügen, eine Art Semaphore volatile zu haben und den Rest
nicht.
> Naja, ist ziemlich unsauber.
Es ist geringfügig umständlich in der Handhabung und vielleicht nicht
allzu schön, aber m.E. durchaus sauber, was den Standard angeht.
> Normalerweise sollte es komplett genügen, eine Art Semaphore volatile> zu haben und den Rest nicht.
Hilft in diesem Fall rein garnicht. Denn an der Optimierung von
Variablen ändert keine Semaphore irgendwas. Nur die Problematik bei
nicht-atomarem Zugriff lässt sich per Sema erledigen.
Und was ISRs angeht, hilft sie auch da nicht, denn was machst du in der
ISR wenn die Sema grad blockiert ist?
Andreas Kaiser wrote:
>> Naja, ist ziemlich unsauber.> Es ist geringfügig umständlich in der Handhabung und vielleicht nicht> allzu schön, aber m.E. durchaus sauber, was den Standard angeht.
Meine Lesart des Standards ist, dass das Ergebnis des ,,Wegcastens''
eines type qualifiers nicht definiert ist.
>> Normalerweise sollte es komplett genügen, eine Art Semaphore volatile>> zu haben und den Rest nicht.> Und was ISRs angeht, hilft sie auch da nicht, denn was machst du in der> ISR wenn die Sema grad blockiert ist?
Ich meinte mehr so eine Art Flag, das im main context gepollt werden
kann und sicherstellt, dass man auf konsistente Daten zugreift.
Eigentlich braucht man volatile doch nur für Dinge, bei denen kein
sequence point dazwischen liegt, also das typische Flag, auf das man
in main() pollt.
U. U. ist gerade für so eine einfache Applikation eine "memory barrier"
sinnvoller zu handhaben als eine Unmenge an volatile-Variablen. Die
"memory barrier" wird implementiert mit:
1
asmvolatile(:::"memory");
Sie zwingt den Compiler, danach alle Werte, die aus dem Speicher
gelesen worden sind, neu zu evaluieren. Leider kann man sie
meiner Meinung nach nicht auf Register anwenden (falls man globale
Registervariablen benutzt). Ich bin mir nicht ganz sicher, ob ein
vergleichbarer Trick für solche Variablen benutzbar wäre:
Das ist nun aber sehr compilerspezifisch. Und für Ersatz von "volatile"
Vars einigermassen umständlich und fehlerträchtig, denn das muss man
dann im gesamten Hauptprogramm an den Zugriffen auf solche Variablen
unterbringen und es setzt gewissen Vorstellungen von der Arbeitsweise
von Compilern voraus. Zudem wirft es alle speicherbezogenen
Optimierungen über Bord, also auch die harmlosen Variablen.
Wenn wegcasten von qualifiern nicht definiert ist, wie gibt man dann
einen String aus (d.h. übergibt ihn an eine Funktion, die "char *"
erwartet)? Immer erst in eine lokale Variable umkopieren? Ist mir noch
nirgends begegnet. Dafür aber jede Menge casts, um "const" loszuwerden
weil die aufgerufene Funktion irgendwann in grauer Vorzeit ohne "const"
deklariert wurde.
Andreas Kaiser wrote:
> Wenn wegcasten von qualifiern nicht definiert ist, wie gibt man dann> einen String aus (d.h. übergibt ihn an eine Funktion, die "char *"> erwartet)? Immer erst in eine lokale Variable umkopieren?
Ja, das ist der einzig saubere Weg, den ich im Standard sehe. Das
ist auch nicht verwunderlich, ist ja schließlich ein Standard, und
der soll die Eckpunkte markieren. Letztlich deutet die Situation,
die du beschreibst, ja auf einen Designfehler hin (gerufene Funktion
ändert zwar die Daten nicht, wurde aber nicht sauber dahingehend
deklariert, sodass man mit einem Hack arbeiten muss). Dein Cast
tötet den Compilertest aber auch für die Fälle, in denen es wirklich
kracht, d. h. die gerufene Funktion ist als non-const char * deklariert,
wird mittels typecast aus einem const char * gefüttert, hackt aber
anschließend tatsächlich auf dem String herum (wozu sie gemäß ihrer
Deklaration berechtigt ist) -> peng!
Da das ,,Wegcasten'' des qualifiers aber ein Designfehler ist, würde
ich dies niemandem für ein Neudesign empfehlen.
Danke für eure Antworten.
Ich habe die volatile Deklaration wieder entfernt, da ich sie nicht
benötige. Da ich meine C Kenntnisse mit CodeVision entwickelt habe, bin
ich nicht so fit in ANSI-C. Aufgrund des Hinnweis im Tutorial habe ich
die deklaration einfach eingefügt ohne weiter darüber nach zu Denken.
Mittlerer Weile bin ich bei einer Codegröße angelangt, die 10% größer
als bei CodeVision ist. Für mich eine vertretbare Größe.
Über die Firma in der ich arbeite werden wir mal versuchen ob Pavel
(Entwickler von CodeVison) eine Komandozeilen-Version seines Compilers
mitanbietet, die dann in die Eclipseumgebeung eingebunden werden kann.
Das währe für mich das Optimum (kleiner Code, max. Flexibilität der
IDE).
Gruß Steffen