PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Gcc fasst untere Register nicht an



s.o.
21.07.2007, 18:40
Hallo,

ich frage mich schon seit längerem, wieso Gcc die unteren Register nie anfässt.(Ich verwende avr-gcc 4.1.2.) Im Wiki finde ich folgendes: R1 – R17, R28, R29 allgemeine Register, die durch einen Funktionsaufruf nicht verändert bzw wieder auf den ursprünglichen Wert restauriert werden. Wenn man jetzt aber globale Variablen hat, legt dieser sie in den SRAM. Wieso diese dann nicht in die unteren Register? Wann (außgenommen Funktionsparameterübergabe) werden diese jemals verwendet. Wie bekomme ich den Gcc dazu, diese zu verwenden. Ich verliere ungern unnötig 16 Register.

Ich hoffe Ihr könnt mir helfen.

uwegw
21.07.2007, 21:21
Könnte damit zusammenhängen, dass die unteren Register nicht alles können, was die oberen können. Für diverse Operationen müsste man daher die Daten aus diesen Registern in eins der oberen Register kopieren und danach wieder zurück. Wahrscheinlich hat man auf die unteren Register verzichtet, um sich diesen Sonderfall zu sparen.

SprinterSB
21.07.2007, 22:53
Was sich im GCC an der Registerallokierung von 3.x nach 4.x geändert hat, kann ich nicht sagen. jedenfalls hat sich das ABI geändert, d.h. die Register werden in der 4.x so verwendet wie in der 3.x.

Globale/Statische Variablen werden nie in Register allokiert, es sei denn, man definiert sich globale Registervariablen à la


int register var asm ("r2");

void foo (void)
{
var >>= 1;
}



foo:
asr r3 ; var
ror r2 ; var
ret ;


Davon ist jedoch tunlichst abzuraten: Nehmen wir an, du verwendest so eine Variable in einer ISR und die IRQ trat in einer LIB-Funktion auf, die so ein Register verwendet. Dann hast du ein Problem...

Ich glaube nicht, daß du die LIBs komplett neu generieren willst und zwar so, daß GCC nirgends die o.g. regs verwendet (abgesehen davon, das Y den Framepionter hält).

Und selbst wenn du keine LIBs verwendest oder sie mit -ffixed= generiert bekommst, ist die Verwendung globaler Registervariablen anders als die globaler Variablen. Es gibt einige Nebeneffekte, die dir ruckzuck das Programm um die Ohren hauen.

GCC verwendet die unteren Register deshalb ungerne, weil die Register nicht alle gleichwertig sind. Es gibt für diese GPRs zB kein LDI und eine konstante in so ein GPR laden geht nur aufwändig über einen sekundären Reload (also über ein oberes GPR).

Die unteren Register werden verwendet, wenn deine Funktionen komplexer werden und wenn du Funktionen hast, die kein Blatt sind:

long long bar (long long a, long long b)
{
return a+b;
}


bar:
/* prologue: frame size=0 */
push r2
push r3
push r4
push r5
push r6
push r7
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
push r16
push r17
/* prologue end (size=16) */
mov r2,r18 ; a, a
mov r3,r19 ; a, a
mov r4,r20 ; a, a
mov r5,r21 ; a, a
mov r6,r22 ; a, a
mov r7,r23 ; a, a
mov r8,r24 ; a, a
mov r9,r25 ; a, a
mov r18,r10 ; b, b
mov r19,r11 ; b, b
mov r20,r12 ; b, b
mov r21,r13 ; b, b
mov r22,r14 ; b, b
mov r23,r15 ; b, b
mov r24,r16 ; b, b
mov r25,r17 ; b, b
mov r10,r2 ; ,
add r10,r18 ; , b
ldi r30,lo8(1) ; tmp46,
cp r10,r2 ; tmp45, a
brlo .L3 ; ,
ldi r30,lo8(0) ; tmp46,
.L3:
mov r11,r3 ; ,
add r11,r19 ; , b
ldi r26,lo8(1) ; tmp47,
cp r11,r3 ; , a
brlo .L4 ; ,
ldi r26,lo8(0) ; tmp47,
.L4:
add r30,r11 ; tmp48,
ldi r31,lo8(1) ; tmp49,
cp r30,r11 ; tmp48,
brlo .L5 ; ,
ldi r31,lo8(0) ; tmp49,
.L5:
or r26,r31 ; tmp47, tmp49
mov r11,r30 ; , tmp48
mov r12,r4 ; ,
add r12,r20 ; , b
ldi r27,lo8(1) ; tmp50,
cp r12,r4 ; , a
brlo .L6 ; ,
ldi r27,lo8(0) ; tmp50,
.L6:
mov r30,r26 ; tmp51, tmp47
add r30,r12 ; tmp51,
ldi r31,lo8(1) ; tmp52,
cp r30,r12 ; tmp51,
brlo .L7 ; ,
ldi r31,lo8(0) ; tmp52,
.L7:
or r27,r31 ; tmp50, tmp52
mov r12,r30 ; , tmp51
mov r13,r5 ; ,
add r13,r21 ; , b
ldi r26,lo8(1) ; tmp53,
cp r13,r5 ; , a
brlo .L8 ; ,
ldi r26,lo8(0) ; tmp53,
.L8:
mov r30,r27 ; tmp54, tmp50
add r30,r13 ; tmp54,
ldi r31,lo8(1) ; tmp55,
cp r30,r13 ; tmp54,
brlo .L9 ; ,
ldi r31,lo8(0) ; tmp55,
.L9:
or r26,r31 ; tmp53, tmp55
mov r13,r30 ; , tmp54
mov r14,r6 ; ,
add r14,r22 ; , b
ldi r27,lo8(1) ; tmp56,
cp r14,r6 ; , a
brlo .L10 ; ,
ldi r27,lo8(0) ; tmp56,
.L10:
mov r30,r26 ; tmp57, tmp53
add r30,r14 ; tmp57,
ldi r31,lo8(1) ; tmp58,
cp r30,r14 ; tmp57,
brlo .L11 ; ,
ldi r31,lo8(0) ; tmp58,
.L11:
or r27,r31 ; tmp56, tmp58
mov r14,r30 ; , tmp57
mov r15,r7 ; ,
add r15,r23 ; , b
ldi r26,lo8(1) ; tmp59,
cp r15,r7 ; , a
brlo .L12 ; ,
ldi r26,lo8(0) ; tmp59,
.L12:
mov r30,r27 ; tmp60, tmp56
add r30,r15 ; tmp60,
ldi r31,lo8(1) ; tmp61,
cp r30,r15 ; tmp60,
brlo .L13 ; ,
ldi r31,lo8(0) ; tmp61,
.L13:
or r26,r31 ; tmp59, tmp61
mov r15,r30 ; , tmp60
mov r16,r8 ; ,
add r16,r24 ; , b
ldi r27,lo8(1) ; tmp62,
cp r16,r8 ; , a
brlo .L14 ; ,
ldi r27,lo8(0) ; tmp62,
.L14:
mov r31,r26 ; tmp63, tmp59
add r31,r16 ; tmp63,
ldi r30,lo8(1) ; tmp64,
cp r31,r16 ; tmp63,
brlo .L15 ; ,
ldi r30,lo8(0) ; tmp64,
.L15:
or r30,r27 ; tmp65, tmp62
mov r17,r9 ; ,
add r17,r25 ; , b
add r30,r17 ; tmp65,
mov r18,r10 ; <result>, <result>
mov r19,r11 ; , <result>
mov r20,r12 ; , <result>
mov r21,r13 ; , <result>
mov r22,r14 ; , <result>
mov r23,r15 ; , <result>
mov r24,r31 ; , <result>
mov r25,r30 ; , tmp65
/* epilogue: frame size=0 */
pop r17
pop r16
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop r7
pop r6
pop r5
pop r4
pop r3
pop r2
ret

Anm: Der Code ist trotz -Os nicht optimal, weil wenig AUfwand in 64-Bit-Typen gesteckt wurde und GCC die Addition algebraisch austextet.

Das Beispiel zeigt aber, daß GCC durchaus diese Register verwendet.

ZU R1: GCC geht davon aus, daß da die 0 drin steht.

SprinterSB
21.07.2007, 22:54
typo: das ABI hat sich natürlich *nicht* geändert!

s.o.
22.07.2007, 20:15
Vielen Dank Sprinter für deine ausfürliche Antwort.

Ich habe noch eine Frage: Da der Gcc4 die unteren Register R2-R17 ja nicht zum Variablenspeichern verwendet, wieso Pushed und Popped(nicht falsch verstehen ;) ) er das dann? Das ist doch Zeit und Resourcenverschwendung?

Grüße

SprinterSB
22.07.2007, 21:13
Ich glaub, da herrscht etwas Verwirrung...

Variablen (d.gh allgemeiner Objekte im Sinne von C) haben einen Speicherort. Globale und statische Variablen leben zB üblicherweise im SRAM (auch möglich sind Flash und EEPROM).

Zur Verarbeitung muss ein Compiler einen Werte von dem Ort, wo er "lebt", in ein Register (GPR) laden, um die gewünschten Operationen damit anzustellen. Das ist notwendig, weil kaum eine Architektur Verarbeitung direkt im RAM erlaubt. AVR zB erlaubt nur Lesen/Schreiben von Werten und das Testen von Bits im bit-adressierbaren SFR-Bereich.

Wird nun eine solche Variable gebraucht, dann muss sie also geladen werden, zB nach R25:R24 für eine 16-Bit-Variable.

Wird nach dem Laden eine Funktion ausgeführt, dann muss avr-gcc den Wert in ein Register sichern, das nicht von der Funktion verändert wird, zB R17:R16. Braucht die aufgerufene Funktion ihrerseits zB R17, muss es gesichert werden, weil der Aufrufer davon ausgeht.

Diese Variablen werden nicht in GPRs angelegt (leben also nicht dort), werden aber evtl durchaus dort hinkopiert.

Lokale Variablen (genauer: automatische Variablen) werden von GCC in GPRs oder im Frame der Funktion angelegt, falls sie nicht wegoptimiert werden.


long foo (long i)
{
if (i)
return foo (i)+i;

return i;
}

foo:
/* prologue: frame size=0 */
push r14
push r15
push r16
push r17
/* prologue end (size=4) */
movw r14,r22 ; i, i
movw r16,r24 ; i, i
cp r22,__zero_reg__ ; i
cpc r23,__zero_reg__ ; i
cpc r24,__zero_reg__ ; i
cpc r25,__zero_reg__ ; i
breq .L2 ; ,
rcall foo ;
add r22,r14 ; <result>, i
adc r23,r15 ; <result>, i
adc r24,r16 ; <result>, i
adc r25,r17 ; <result>, i
rjmp .L1 ;
.L2:
.L1:
/* epilogue: frame size=0 */
pop r17
pop r16
pop r15
pop r14
ret


Mal abgesehen davon, daß das Beispielprogramm recht sinnlos ist: GCC macht hier guten Code und das Sichern der Register (i in R22-R25 nach R14-R17) um den Aufruf von foo ist nicht überflüssig.

Hast du ein konkretes, überschaubares Beispiel da, von dem du denkst, daß GCC schlecht arbeitet? Vielleicht kann man daran diskutieren, was abgeht.

Übersetzen am besten mit -O2 oder -Os zusammen mit -S und -fverbose-asm.

s.o.
23.07.2007, 06:42
Hallo Sprinter.

Danke für die ausfürliche Antwort.

Zu deinem Bsp.
Wieso muss Gcc diese sichern? Das ganze geht ja auch ohne das sichern. Es geht doch auch add R22,R22 ?

Ich habe mal ein Beispiel, bei dem der Gcc (für mich) unverständlich viel Pushed.


long foo(long a, long b, long c, uint8_t d){
if(d){
return a+b;
}else{
return a-c;
}
}
Nun das Listing:


long foo(long a, long b, long c, uint8_t d){
4e: cf 92 push r12 ;Alle unteren Register werden gepushed
50: ef 92 push r14 ;obwohl sie nicht verändert werden
52: ff 92 push r15 ;
54: 0f 93 push r16
56: 1f 93 push r17
if(d){
58: cc 20 and r12, r12
5a: 29 f0 breq .+10 ; 0x66 <foo+0x18>
return a+b;
5c: 62 0f add r22, r18
5e: 73 1f adc r23, r19
60: 84 1f adc r24, r20
62: 95 1f adc r25, r21
64: 04 c0 rjmp .+8 ; 0x6e <foo+0x20>
}else{
return a-c;
66: 6e 19 sub r22, r14
68: 7f 09 sbc r23, r15
6a: 80 0b sbc r24, r16
6c: 91 0b sbc r25, r17
6e: 1f 91 pop r17 ;und wieder restaurieren, obwohl sie
70: 0f 91 pop r16 ;unberührt sind...
72: ff 90 pop r15
74: ef 90 pop r14
76: cf 90 pop r12
78: 08 95 ret


Gcc ist jedoch, obwohl er diese Eigenheit hat. einer der besten Compiler. Recursive funktionen macht er z.T. auch ohne Pushen. Sofern halt wieder die unteren Register nicht berührt werden.

Mir ist bewust, dass Gcc die unteren Register nur zum Teil verwenden kann. Das ist doch aber kein Grund, diese zu Pushen, obwohl die nicht verändert werden. Kann man das dem Gcc irgendwie abgewöhnen?

Grüße

SprinterSB
23.07.2007, 08:48
Ja, hier werden die Register unnötigerweise gesichert, weil sie nicht verändert werden.

IMHO ist das eine Unzulänglichkeit in GCC (also nicht nur in avr-gcc).

Ich kann die erläutern, wo das Problem herkommt, und wo du es fixen kannst:

Du übersetzt mit -da -dp -save-temps. Das erzeugt dir zig RTL (register transfer language) dumps.
Einer der komplexesten Teile von GCC (und vieler anderer optimierender Compiler) ist die Register-Allokierung. In diesen Passes werden die unendlich vielen Pseudo-Register, in denen die Werte gehalten werden, auf die endlich vielen Hard-Register der Maschine abgebildet. Falls die Hard-Register nicht ausreichen, landen die Werte in Stack-Slots und müssen zur Verabreitung vom Stack geladen werden, wofür dann wieder andere Hard-Register frei geschaufelt werden müssen, etc.

In Pass lreg (local register allocation = reg-alloc innerhalb von basic blocks) ist noch alles ok, zB

(insn 5 4 6 0 (set (reg/v:SI 44 [ c ])
(reg:SI 14 r14 [ c ])) 14 {*movsi} (nil)
(expr_list:REG_DEAD (reg:SI 14 r14 [ c ])
(nil)))

(reg 44) ist ein SI-Pseudo (also 32-Bit) und wird geladen vom SI-Hard-Reg 14, einem incoming arg (=c) von foo. In dieser insn stirbt R14 (also R14...R17), weil es jetzt ja in R44 steht. Ich vereinfache mal das insn-Pattern auf das Wesentliche:


(set (reg:SI 44)
(reg:SI 14))

In Pass greg+postreload (global register allocation) wird daraus

(set (reg:SI 14)
(reg:SI 14))
d.h. GCC allokiert R44 nach R14 (eine gute Wahl) und in flow2 wird erkannt, daß insn 5 trivial ist und weggeworfen. Allerdings lebt R14 weiterhin, weil es ja verwendet wird (wenn auch nirgends explizit gesetzt wird).

In ./gcc/config/avr/avr.c:function_prologue() und :function_epilogue() wird der asm-Code für Funktions-Prolog und -Epilog ausgegeben, der sich u.a. darum kümmert, Regs die leben *und* call save sind (als nach einer Funktion unverändert vorliegen) zu sichern, d.h. dort werden die überflüssigen push/pop emittiert.

Die Info, ob ein (pseudo oder hard) Reg lebt, wird in regs_ever_live[] gemerkt. Und dort steht eben nur, ob ein Reg lebt, aber nicht, ob es verändert wird oder nicht.

:idea: Um den Code zu verbessern, müsste man also an dieser Stelle nicht auf regs_ever_live[] testen, sondern eine neue live analysis durchführen und testen, ob für diese Register REG_N_SETS() gleich Null ist und einen neuen avr-gcc generieren (bzw ein neuer cc1, das reicht schon). Möglicherweise sieht GCC bzw. die zu REG_N_SETS() betragenden Funktionen in ./gcc/regclass.c, daß R14 zu den virtual-incoming-args Registern gehört, und zählen es zu den gesetzten Registern hinzu.

Falls du an GCC nicht selbst Hand anlegen willst, kannst du bei http://gcc.gnu.org eine feature-request machen und bei der GCC 4.3 hast du schon besseren Code. (AFAIK ist momentan jedoch Konsolidierung und ein feature freeze).

s.o.
23.07.2007, 20:07
Hallo Sprinter.

Ich traue es mir nicht zu, im Gcc zumzufummeln. Da habe ich zu wenig Erfahrung dazu. Ich mag mich zwar etwas auszennen, aber das ist doch dann zu kompliziert.

Ich werde wohl bei Gcc einen features-request einreichen, und hoffen das die Verstehen, was ich meine.^^

Vielen Dank für deine Hilfe.

SprinterSB
28.07.2007, 18:12
Falls es nur wenige, zeitkritische Funktionen sind, um die es geht, bleiben noch folgende Möglichkeiten, ohne GCC anzufassen:

-- Funktion(en) unter Beachtung des avr-ABI in Assembler / Inline Assembler formulieren. Ob ne Stub zusammen mit Inline Asm hilft, müsste man testen (da besteht uU das Problem weiterhin).
-- Funktion(en) inlinen, zb mit "inline" oder "__attribute__((always_inline))"

s.o.
29.07.2007, 05:45
Hallo Sprinter,

das mit dem I-ASM ist eine gute Idee für zeitkritische Funktionen.

Der Gcc Bug Report:
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=32871

Wo Du gerade das Attribute always_inline ansprichst. Angenommen, man hat eie Funktion, die immer geinlined wird. Dann ist aber immer noch die Funktion im Listing an sich da, wieso löscht der Avr-Gcc die dann nicht?

Grüße

SprinterSB
29.07.2007, 10:25
Die Funktion muss ja da bleiben, weil es vielleicht eine externe Referenz auf die Funktion gibt. Hier hilft's, die Funktion als static zu geklarieren. Aber selbst dann kann die Funktion noch gebraucht werden, zB wenn ihre Adresse genommen wird.

Dann gibt's noch die hilfreichen Schalter -Winline -fno-keep-inline-functions (für -fxxx gibt's immer auch -fno-xxx)

Weitere Rädchen zum Feinjustieren siehst du mit


avr-gcc -v --help | grep inline


Aber aufpassen: grep filtert nur nach "inline". Evtl sind die Options nur für C++. Dann das komplette avr-gcc -v --help lesen...

SprinterSB
29.07.2007, 10:34
Zum bugreport bei gcc.gnu.org:

Häng da noch die Precompilierte Quelle (also wo alle #includes etc aufgelöst sind) dran, und wie du die Datei erhalten hast!

Sonst kann man das niemals nachvollziehen!



long foo (long a, long b, long c, unsigned char d){
if(d){
return a+b;
}else{
return a-c;
}
}



d:\avr\test>avr-gcc blah.c -Os -S -v -fverbose-asm
Reading specs from E:/WinAVR_20060421/lib/gcc/avr/3.4.6/specs
Configured with: ../gcc-3.4.6/configure --prefix=/c/WinAVR --target=avr --enable
=c,c++ --with-dwarf2 --enable-win32-registry=WinAVR --disable-nls
Thread model: single
gcc version 3.4.6
E:/WinAVR_20060421/libexec/gcc/avr/3.4.6/cc1.exe -quiet -v -iprefix e:\WinAVR_2
n/../lib/gcc/avr/3.4.6/ blah.c -quiet -dumpbase blah.c -auxbase blah -Os -versio
e-asm -o blah.s
ignoring nonexistent directory "E:/WinAVR_20060421/avr/sys-include"
#include "..." search starts here:
#include <...> search starts here:
e:\WinAVR_20060421\bin/../lib/gcc/avr/3.4.6/include
E:/WinAVR_20060421/lib/gcc/avr/3.4.6/include
E:/WinAVR_20060421/avr/include
End of search list.
GNU C version 3.4.6 (avr)
compiled by GNU C version 3.4.2 (mingw-special).
GGC heuristics: --param ggc-min-expand=47 --param ggc-min-heapsize=32702

blah.s:

.file "blah.c"
.arch avr2
__SREG__ = 0x3f
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__tmp_reg__ = 0
__zero_reg__ = 1
.global __do_copy_data
.global __do_clear_bss
; GNU C version 3.4.6 (avr)
; compiled by GNU C version 3.4.2 (mingw-special).
; GGC heuristics: --param ggc-min-expand=47 --param ggc-min-heapsize=32702
; options passed: -v -iprefix -auxbase -Os -fverbose-asm
; options enabled: -feliminate-unused-debug-types -fdefer-pop
; -fomit-frame-pointer -foptimize-sibling-calls -funit-at-a-time
; -fcse-follow-jumps -fcse-skip-blocks -fexpensive-optimizations
; -fthread-jumps -fstrength-reduce -fpeephole -fforce-mem -ffunction-cse
; -fkeep-static-consts -fcaller-saves -freg-struct-return -fgcse
; -fgcse-lm -fgcse-sm -fgcse-las -floop-optimize -fcrossjumping
; -fif-conversion -fif-conversion2 -frerun-cse-after-loop
; -frerun-loop-opt -fdelete-null-pointer-checks -fsched-interblock
; -fsched-spec -fsched-stalled-insns -fsched-stalled-insns-dep
; -fbranch-count-reg -freorder-functions -fcprop-registers -fcommon
; -fverbose-asm -fregmove -foptimize-register-move -fargument-alias
; -fstrict-aliasing -fmerge-constants -fzero-initialized-in-bss -fident
; -fpeephole2 -fguess-branch-probability -fmath-errno -ftrapping-math
; -minit-stack=__stack -mmcu=avr2

.text
.global foo
.type foo, @function
foo:
/* prologue: frame size=0 */
push r12
push r14
push r15
push r16
push r17
push r28
push r29
/* prologue end (size=7) */
mov r29,r25 ; a, a
mov r28,r24 ; a, a
mov r27,r23 ; a, a
mov r26,r22 ; a, a
tst r12 ; d
breq .L2 ; ,
mov r22,r26 ; <result>, a
mov r23,r27 ; <result>, a
mov r24,r28 ; <result>, a
mov r25,r29 ; <result>, a
add r22,r18 ; <result>, b
adc r23,r19 ; <result>, b
adc r24,r20 ; <result>, b
adc r25,r21 ; <result>, b
rjmp .L1 ;
.L2:
mov r22,r26 ; <result>, a
mov r23,r27 ; <result>, a
mov r24,r28 ; <result>, a
mov r25,r29 ; <result>, a
sub r22,r14 ; <result>, c
sbc r23,r15 ; <result>, c
sbc r24,r16 ; <result>, c
sbc r25,r17 ; <result>, c
.L1:
/* epilogue: frame size=0 */
pop r29
pop r28
pop r17
pop r16
pop r15
pop r14
pop r12
ret
/* epilogue end (size=8) */
/* function foo size 38 (23) */
.size foo, .-foo
/* File "blah.c": code 38 = 0x0026 ( 23), prologues 7, epilogues 8 */

s.o.
29.07.2007, 17:13
Hallo Sprinter,

ich habe den Bugreport erweitert. Vielen Dank für Deinen Hinweis!

Michael