Hallo zusammen
Ich schreibe am Code für ein AVR Atmel 328p (Arduino Projekt).
Inzwischen habe ich zig funktionen in denen ich separat die variable
1
inti;
initialisiere, definiere und für Loop-Schleifen brauche.
Wäre eine globale Definition nicht einfacher und sparender im RAM?
wie handhabt der Compailer meiner massen-definitionen in den Funktionen?
Beste Grüsse,
Markus
markus zelg schrieb:> Hallo zusammen>> Ich schreibe am Code für ein AVR Atmel 328p (Arduino Projekt).> Inzwischen habe ich zig funktionen in denen ich separat die variable>
1
inti;
initialisiere, definiere und für Loop-Schleifen brauche.
>> Wäre eine globale Definition nicht einfacher und sparender im RAM?> wie handhabt der Compailer meiner massen-definitionen in den Funktionen?
Die Variable wird erzeugt, und wenn sie aus dem Scope geht, wird sie
wieder gekillt.
So aufwändig ist dieser Vorgang auch wieder nicht, dass du dir da jetzt
groß Geddanken darüber machen musst.
Auf jeden Fall ist das viel besser, als wenn dir die einzelnen i-s, die
in den Funktionen eigentlich unabhängig voneinanander sein solltem ,
sich gegenseiteig in die Quere kommen, weil sie
* alle auf dieselbe Variable führen
* eine Funktion (die dieses i benutzt) eine andere Funktion aufruft
(die ebenfalls dieses i benutzt), welche wieder eine Funktion
benutzt (die auch dieses i benutzt), sich die einzelnen Funktionen
gegenseitig die Werte für i unter dem Allerwertesten ändern und
alles den Bach runter geht.
markus zelg schrieb:> Wäre eine globale Definition nicht einfacher und sparender im RAM?
Nein.
> wie handhabt der Compailer meiner massen-definitionen in den Funktionen?
Er reserviert sich innerhalb der Funktion ein Register für den
Schleifenzähler, bzw. zwei, da int zwei Bytes groß ist.
Eine globale Variable würde bedeuten, dass keine deiner Funktionen eine
andere aufrufen darf. Sie würden sich nämlich gegenseitig das i
kaputtmachen.
Lokale Variablen landen auf dem Stack, d.h. der Speicher ist nur so
lange reserviert, wie eine Funktion gerade ausgeführt wird.
Sam P. schrieb:> Lokale Variablen landen auf dem Stack, d.h. der Speicher ist nur so> lange reserviert, wie eine Funktion gerade ausgeführt wird.
Wenn überhaupt!
Denn:
Rolf Magnus schrieb:> Er reserviert sich innerhalb der Funktion ein Register für den> Schleifenzähler
optimiert der Compiler eigentlich auch die Datentypen?
dH benutzt er statt dem deklariertem int ein int8, wenn die schleife mit
Konstanten definiert ist?
Vlad Tepesch schrieb:> optimiert der Compiler eigentlich auch die Datentypen?> dH benutzt er statt dem deklariertem int ein int8, wenn die schleife mit> Konstanten definiert ist?
Er kann und wird sie in entsprechender Optimierungseinstellung sogar
ganz entfernen. (Loop Unrolling)
Der Datentyp kann selten darüber hinaus wirklich geändert werden.
Bedenke, dass Zuweisungen und Manipulationen von/mit dem Schleifenzähler
sich mit unterschiedlichen Typen unterscheidlich verhalten und erstmal
nicht identisch sind.
markus zelg schrieb:> Wäre eine globale Definition nicht einfacher und sparender im RAM?
Der AVR arbeitet am einfachsten und schnellsten mit seinen Registern.
Ein RAM-Zugriff erfordert mehr Aufwand (mehr Code) wegen der
Load/Store-Architektur.
D.h. wenn eine Funktion alle ihre lokalen Variablen in Registern halten
kann, hast Du den optimalen Code.
Ganz abgesehen davon: Beim modularen Programmieren soll jede Funktion
ihre privaten Daten (wie lokale Variablen) gegenüber dem Rest des
Systems kapseln, damit niemand von außen Schabernack damit treiben kann.
Maxx schrieb:> Er kann und wird sie in entsprechender Optimierungseinstellung sogar> ganz entfernen. (Loop Unrolling)
bei -Os unwahrscheinlich, zumindest für größere n
Vlad Tepesch schrieb:> dH benutzt er statt dem deklariertem int ein int8, wenn die schleife mit> Konstanten definiert ist?
Nö.
Er jongliert dann auf nem 8Bitter fleißig mit den unnötigen höheren
Bytes rum, d.h. addiert 0, vergleicht mit 0 usw.
Der AVR-GCC macht allerdings gerne mal das Gegenteil, d.h. er bläht bei
bestimmten Operationen die 8Bit unnötig auf 16Bit auf.
Ein globales uint8_t i ist sehr teuer. Der Compiler holt es immer aus
dem SRAM in ein Register, macht was und schreibt es wieder zurück:
Load + Operation + Store = 5 Zyklen, 10 Byte Code, 1 Byte SRAM
Ein lokales i kann er dagegen oft in einem Register halten, d.h. es
belegt keinen SRAM und das ständige Load/Store entfällt:
Operation = 1 Zyklus, 2 Byte Code, 0 Byte SRAM
Peter
Peter Dannegger schrieb:> Nö.> Er jongliert dann auf nem 8Bitter fleißig mit den unnötigen höheren> Bytes rum, d.h. addiert 0, vergleicht mit 0 usw.>> Der AVR-GCC macht allerdings gerne mal das Gegenteil, d.h. er bläht bei> bestimmten Operationen die 8Bit unnötig auf 16Bit auf.
Eigentlich macht er da genau das gleiche. Er erweitert halt erstmal
alles auf int, wie C es vorschreibt, optimiert aber eben nicht überall,
wo es möglich wäre, diese Erweiterung wieder weg.
> Ein globales uint8_t i ist sehr teuer. Der Compiler holt es immer aus> dem SRAM in ein Register, macht was und schreibt es wieder zurück:
Nur wenn es volatile ist. Sonst optimiert er die Zugriffe bei
Zwischenwerten, wie sie in einer Schleife beim Zähler ja recht häufig
vorkommen, durchaus auch weg. Ich würde mal schätzen, daß letztendlich
nur ein einzelner Schreibzugriff nach der Schleife mit dem Endwert des
Schleifenzählers durchgeführt wird.
Das passiert aber auch nur, weil du innerhalb der Schleife abhängig von
einer anderen Bedinung ein return machst. Dann macht der Compiler bei
jedem Durchlauf einen Schreibzugriff, weil die Funktion mittendrin
verlassen werden könnte. Lesezugriff macht er aber auch hier entgegen
deiner Behauptung keinen einzigen.
Ich bin selber erstaunt, wie sehr der Optimierer durch die globale
Schleifenvariable aus dem Tritt kommt.
Es hätten eigentlich nur 4 Bytes mehr sein dürfen, aber nicht 22.
Peter
Hallo
Besten Dank für die vielen Beiträge.
Werde auf lokale Initialisierung bleiben.
Rolf Magnus schrieb:> Eigentlich macht er da genau das gleiche. Er erweitert halt erstmal> alles auf int, wie C es vorschreibt, optimiert aber eben nicht überall,> wo es möglich wäre, diese Erweiterung wieder weg.
Ja, erst neulich habe ich alle for(int i=0) auf byte i=0 gesetzt und
siehe da, schon sind wesentlich viel ROM Flash frei geworden. Was der
freeMemory() meldet habe ich noch nicht angeschaut.
Rolf Magnus schrieb:> Das passiert aber auch nur, weil du innerhalb der Schleife abhängig von> einer anderen Bedinung ein return machst. Dann macht der Compiler bei> jedem Durchlauf einen Schreibzugriff, weil die Funktion mittendrin> verlassen werden könnte. Lesezugriff macht er aber auch hier entgegen> deiner Behauptung keinen einzigen.
Das wird in der Regel gelöst, indem das "Aufräumen am Ende einer
Funktion gemeinsam genutzt wird. Das macht üblicher Weise auch GCC so
für andere Plattformen. In dieser könnte und sollt er auch i sichern.
Das "Return" übersetzt er ja auch nicht mit "ret" sondern einem "rjmp"
zum Aufräumen/Verlassen. Das Speichern in jedem Durchlauf ist also nicht
benötigt.
Rolf Magnus schrieb:> Dann macht der Compiler bei> jedem Durchlauf einen Schreibzugriff, weil die Funktion mittendrin> verlassen werden könnte.
Nein, macht er nicht. Nur am Anfang und nach der 1. Abbruchbedingung
oder nach der 2.
Aber er verliert völlig den Faden und und macht unnötige
Zwischenschritte. Er benutzt sogar ein Register mehr.
Peter
Maxx schrieb:> Das "Return" übersetzt er ja auch nicht mit "ret" sondern einem "rjmp"> zum Aufräumen/Verlassen
Nein, er übersetzt es durchaus mit einem ret. Das rjmp springt nicht zum
Aufräumen, sondern es überspringt das ret.
Peter Dannegger schrieb:> 9a: 98 2f mov r25, r24> 9c: 91 50 subi r25, 0x01 ; 1
Hier wird der Schleifenzähler dekrementiert.
> 9e: 48 9b sbis 0x09, 0 ; 9> a0: 03 c0 rjmp .+6 ; 0xa8 <iglobal+0x18>
Das ist die Prüfung von PIND. Das rjmp springt über's return, sofern
PIND0 gesetzt ist.
> a2: 80 93 00 01 sts 0x0100, r24
Speichern des Zählers.
> a6: 08 95 ret
Und das return im if.
> a8: 89 2f mov r24, r25>> aa: 88 23 and r24, r24> ac: a9 f7 brne .-22 ; 0x98 <iglobal+0x8>
Rücksprung zum Anfang der Schleife.
> ae: 10 92 00 01 sts 0x0100, r1> b2: 08 95 ret
Und das ist das Aufräumen am Schluß.
Peter Dannegger schrieb:> Nein, macht er nicht. Nur am Anfang und nach der 1. Abbruchbedingung> oder nach der 2.
Stimmt.
> Aber er verliert völlig den Faden und und macht unnötige> Zwischenschritte. Er benutzt sogar ein Register mehr.
Das macht er, weil er das Dekrementieren des Registers am Anfang macht,
aber beim Abbruch in der Variable den Wert von vor dem Dekremeniteren
speichern muß. Deshalb nimmt er r25 als temporäres Zwischenregister für
den dekrementieren Wert. Das könnte sicher schöner gelöst werden, vor
allem, weil er am Schluß dann noch ein zusätzliches "and" einfügt, nur
um die Flags für's brne zu setzen. Würde er dort dekrementieren, dann
könnte einiges wegfallen.
Rolf Magnus schrieb:>> 9e: 48 9b sbis 0x09, 0 ; 9>> a0: 03 c0 rjmp .+6 ; 0xa8 <iglobal+0x18>>> Das ist die Prüfung von PIND. Das rjmp springt über's return, sofern> PIND0 gesetzt ist.
Grmpf... das hätte natürlich heißen sollen: "... sofern PIND0 nicht
gesetzt ist.