Hallo,
ich habe das Thema "Compiler" von gestern hier nochmal aufgearbeitet.
Ganz simples Programm: Es wird etwas an den IOs eingestellt und etwas an
den Timerm gemacht. Wenn ich die Funktion init_AVR nicht static
deklariere, erzeugt der Compiler ineffizienteren Code als mit static.
Nämlich z.B. hier:
ohne static
1
// Input Capture
2
TIFR1 |= (1<<ICF1);
3
ac: b5 9a sbi 0x16, 5 ; 22
4
TIMSK1 |= (1<<ICIE1);
5
ae: ef e6 ldi r30, 0x6F ; 111
6
b0: f0 e0 ldi r31, 0x00 ; 0
7
b2: 80 81 ld r24, Z
8
b4: 80 62 ori r24, 0x20 ; 32
9
b6: 80 83 st Z, r24
10
TCCR1B |= (1<<ICNC1);
11
b8: e1 e8 ldi r30, 0x81 ; 129
12
ba: f0 e0 ldi r31, 0x00 ; 0
13
bc: 80 81 ld r24, Z
14
be: 80 68 ori r24, 0x80 ; 128
15
c0: 80 83 st Z, r24
16
TCCR1B |= (1<<CS11); // Prescaler 8
17
c2: 80 81 ld r24, Z
18
c4: 82 60 ori r24, 0x02 ; 2
19
c6: 80 83 st Z, r24
20
c8: 08 95 ret
mit static:
1
// Input Capture
2
TIFR1 |= (1<<ICF1);
3
ac: b5 9a sbi 0x16, 5 ; 22
4
TIMSK1 |= (1<<ICIE1);
5
ae: 80 91 6f 00 lds r24, 0x006F
6
b2: 80 62 ori r24, 0x20 ; 32
7
b4: 80 93 6f 00 sts 0x006F, r24
8
TCCR1B |= (1<<ICNC1);
9
b8: 80 91 81 00 lds r24, 0x0081
10
bc: 80 68 ori r24, 0x80 ; 128
11
be: 80 93 81 00 sts 0x0081, r24
12
TCCR1B |= (1<<CS11); // Prescaler 8
13
c2: 80 91 81 00 lds r24, 0x0081
14
c6: 82 60 ori r24, 0x02 ; 2
15
c8: 80 93 81 00 sts 0x0081, r24
Kann mir jemand sagen warum das so ist?
Hier nochmal der Output:
Building file: .././main.c
Invoking: AVR/GNU C Compiler : 4.9.2
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-gcc.exe" -x c
-funsigned-char -funsigned-bitfields -DDEBUG -DF_CPU=8000000 -Os
-ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2
-Wall -mmcu=atmega328p -c -std=gnu99 -MD -MP -MF "main.d" -MT"main.d"
-MT"main.o" -o "main.o" ".././main.c"
Finished building: .././main.c
Building target: 1V0.elf
Invoking: AVR/GNU Linker : 4.9.2
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-gcc.exe" -o
1V0.elf main.o -Wl,-Map="1V0.map" -Wl,--start-group -Wl,-lm
-Wl,--end-group -Wl,--gc-sections -mmcu=atmega328p
Finished building target: 1V0.elf
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-objcopy.exe" -O
ihex -R .eeprom -R .fuse -R .lock -R .signature -R .user_signatures
"1V0.elf" "1V0.hex"
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-objcopy.exe" -j
.eeprom --set-section-flags=.eeprom=alloc,load --change-section-lma
.eeprom=0 --no-change-warnings -O ihex "1V0.elf" "1V0.eep" || exit 0
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-objdump.exe" -h
-S "1V0.elf" > "1V0.lss"
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-objcopy.exe" -O
srec -R .eeprom -R .fuse -R .lock -R .signature -R .user_signatures
"1V0.elf" "1V0.srec"
"C:\Users\ingo\Downloads\Atmel
Studio\Toolchain\Atmel_AVR_Toolchain_3.5.0.1662\bin\avr-size.exe"
"1V0.elf"
text data bss dec hex filename
210 0 0 210 d2 1V0.elf
Done executing task "RunCompilerTask".
Task "RunOutputFileVerifyTask"
Program Memory Usage : 210 bytes 0,6 % Full
Data Memory Usage : 0 bytes 0,0 % Full
(sinngemäß)
> wenn man eine Funktion static macht, erzeugt der Compiler besseren Code
Ja, deshalb gibt es static. Es sagt nämlich:
"außer in dieser Übersetzungseinheit wird diese Funktion nirgends
gebraucht"
Damit kann man sie problemlos inlinen und wenn sie nur einmal benutzt
wird, spart man sich CALL/RET inklusive der
Register-Sicherungskonventionen.
Erste Optimierungsregel: sag dem Compiler genau was du tun willst.
(ok, zugegeben, in deinem Beispiel stellt sich derGCC schon etwas an.
Das war wirklich beides -Os?)
Carl D. schrieb:>> wenn man eine Funktion static macht, erzeugt der Compiler besseren Code
Eigentlich war hier eher die Frage, warum er die Registerzugriffe über
den Z-Pointer macht.
Carl D. schrieb:> Das war wirklich beides -Os?)
Jepp
Na ja, warum er da ohne static auf die Register indirekt zugreift, mit
static dagegen direkt, hat mit der Sichtbarkeit der Funktion nichts zu
tun.
Allerdings gilt hier eine ganz pragmatische Lebensregeln: Frag einfach
niemals, warum ein Compiler das macht, was er macht, schon gar nicht
beim gcc (außer, du bist Compilerbauer). Es lohnt sich schlicht nicht,
und vergeudet nur unnütze Lebenszeit.
Oliver
Oliver S. schrieb:> Allerdings gilt hier eine ganz pragmatische Lebensregeln: Frag einfach> niemals, warum ein Compiler das macht, was er macht, schon gar nicht> beim gcc (außer, du bist Compilerbauer). Es lohnt sich schlicht nicht,> und vergeudet nur unnütze Lebenszeit.
Ich dachte mir, dass genau solche kleinen Finessen den Compiler über
kurz oder lang verbessern... Es muss ja nicht immer ein Fehler sein,
den man verbessert. Ich sehe sowas eher als eine Art Optimierung.
Dieses Verhalten ist mir auch schon aufgefallen.
Meine bisherige Analyse: Die Optimierung, statt über das Z-Register
direkt mit LDS/STS auf das Register zuzugreifen, scheint nur dann
stattzufinden, wenn die betreffende Funktion mindestens eine Verzweigung
enthält.
Die Funktion init_AVR enthält keine Verzweigung, also wird dort der
Zugriff nicht optimiert. Durch die Static-Deklaration wird die Funktion
init_AVR aber gar nicht erzeugt, sondern stattdessen der entsprechende
Programmcode direkt in main eingebaut. Da main aber eine Verzweigung in
Form der While-Schleife enthält, wird hier der Zugriff optimiert.
Durch die fehlende Optimierung wird der Programmcode nicht größer, es
werden nur zwei zusätzliche Taktzyklen pro Registerzugriff gebraucht. In
einer Init-Funktion, die nur ein einziges Mal aufgerufen wird, ist das
nicht weiter tragisch.
Ansonsten enthält jede größere Funktionen i.Allg. mindestens eine
Verzweigung. Bei kleineren zeitkritischen Funktionen ist man sowieso
bestrebt, diese vom Compiler inlinen zu lassen, und die Chance ist groß,
dass dann die übergeordnete Funktion eine Verzweigung enthält.
Erfolgt nur ein einzelner Lese- oder Schreibzugriff auf das Register,
wird immer optimiert. Erfolgen mehr als zwei Zugriffe auf dasselbe
Register, ist die Verwendung des Z-Registers zum Zwischenspeichern der
Adresse durchaus sinnvoll, da dies kürzerem Code ergibt.
Das seltsame Verhalten des Compiler ist also nur in seltenen Fällen ein
Problem.
Mir fällt spontan nur ein einziger Fall ein, wo dieses Verhalten stört,
nämlich in Interrupthandlern. Diese sind zwar i.Allg. kurz, können aber
trotzdem nicht geinlinet werden. Auf Grund ihrer Einfachheit enthalten
sie oft auch keine Verzweigungen. Durch die fehlende Optimierung werden
nicht nur die oben erwähnten zusätzlichen zwei Taktzyklen benötigt, da
die Register R30 und R31 auf dem Stack gesichert werden müssen. Das
kostet weitere 8 Zyklen plus 8 Bytes Flash. In besonders zeitkritischen
Fällen könnte man deswegen darüber nachdenken, in den Interrupthandler
eine Dummy-Verzweigung einzubauen.
Vielleicht kommt ja Johann noch vorbei und kann aus Expertensicht etwas
dazu sagen.
wo man den Compiler definitiv unterstützen kann, ist wenn man als
volatile deklarierte Register (alle HW-Register) nicht bitweise füllt.
Wenn man alle Timer-Register initialisiert, dann darf man die Werte
zuerst ver-oder-n und dann zuweisen. Im anderen fall sagt man: "bitte in
diesem sich jederzeit ändernden Register ein Bit setzen", also
Read/Modify/Write. Und so macht er das dann eben.
Das mit dem Z-Pointer könnte auch daher kommen, daß eine Funktion R30/31
frei benutzen darf. Der Aufrufende muß diese sichern, falls er sie noch
braucht. Als inline-Funktion sieht dann die Kostenberechnung ganz anders
aus. Und -Os bedeutet "Size", was offenbar als "Speed egal" gedeutet
wird. Aus Size-Sicht sind beide gleichwertig, vermutlich gewinnt der
Erst-Beste ;-)
Ingo L. schrieb:> Ich dachte mir, dass genau solche kleinen Finessen den Compiler über> kurz oder lang verbessern... Es muss ja nicht immer ein Fehler sein,> den man verbessert. Ich sehe sowas eher als eine Art Optimierung.
Der Compiler kann deine Beiträge leider nicht lesen, und sich von selber
verbessern kann er auch nicht. Der offizielle Weg wäre also die
Erstelling eines Bug-Reports, mit der Hoffnung, daß sich einer der
Compilerentwickler der Sache annimmt.
Leider gibt es die für den AVR-Gcc nicht wirklich. Johann L.
ausdrücklich ausgenommen (ohne ihn wäre der AVR-gcc wohl noch auf dem
Stand des letzen Jahrtausends), aber der kann die Welt auch nicht
alleine retten.
Also bleibt für dich nur Plan B: gcc-Sourcen besorgen, verstehen, selber
verbessern...
Oliver
Leider sind Bugreports beim GCC nur wegen "Optimierungspotenzial"
ziemlich aussichtslos. Das liegt natürlich hauptsächlich am kleinen
Entwicklerstab und man kann es auch nachvollziehen, dass wegen ein paar
Bytes dann nix gemacht wird.
Ich habe da auch schon einige Merkwürdigkeiten gefunden.
Was ich aber gemerkt habe: Mit neueren Versionen ändert sich das
Verhalten durchaus mal. Auch wenn's nicht explizit irgendwo beschrieben
ist. Einfach mal ein bisschen rumtesten. Teste mal eine 5er Version.
Auch mit Optimierungsparametern spielen.