Guten Morgen,
diesmal ist die Effizienz die Frage, die mich umtreibt. Ich habe eine
ISR, die immer mit einer Millisekunde Takt ausgeführt wird, wobei
unterschiedliche, kurze Funktionen ausgeführt werden müssen.
Im Moment sieht das ungefähr so aus:
1
// Liste aller momentan in ISR ausfuehrbarer Funktionen
2
typedefvoid(*voidFnct_t)(void);
3
voidFnct_tgl_isrfcns[8];
4
5
6
ISR(TIMER0_OVF_vect)// Takt 1kHz
7
{
8
9
while(*gl_isrfcns){
10
voidFnct_tfkt=*gl_isrfcns;
11
gl_isrfcns++;
12
fkt();
13
}
14
}
Das ist sehr komfortabel, da ich im Array "gl_isrfcns" einfach nur die
benoetigten Funktionspointer anhängen oder wieder entfernen muß und die
eigentliche Auswahl dessen, was in der ISR ausgeführt werden muß in den
entsprechenden Modulen stattfindet. Nur habe ich den Overhead durch die
Funktionszeiger.
Oder ist es sinnvoller auf ein anderes Konstrukt zu schwenken:
1
ISR(TIMER0_OVF_vect)// Takt 1kHz
2
{
3
if(gl_isfkt0inisr)fkt0();
4
if(gl_isfkt1inisr)fkt1();
5
if(gl_isfkt2inisr)fkt2();
6
if(gl_isfkt3inisr)fkt3();
7
if(gl_isfkt4inisr)fkt4();
8
if(gl_isfkt5inisr)fkt5();
9
if(gl_isfkt6inisr)fkt6();
10
}
was weniger modular aber vermutlich schneller wäre?
Viele Grüße
Nicolas
Und woher weist du, dass deine von der ISR aufgerufenen Funktionen in
der Zeit fertig werden und dass du nicht die nächste Funktion aufrufst
während die vorherige noch läuft?
Deshalb ruft man aus Interruptroutinen keine Funktionen auf!
Du kannst aber in der ISR ein Flag setzen, das in der Hauptschleife
kontrolliert und abgearbeitet wird, wenn bestimmte Bedingungen erfüllt
sind.
Guten Morgen,
ich schließe mich dem vorherigen Eintrag an, ereignisgesteuertes
Programmieren in der Hauptprogrammschleife ist i.A. der zu wählende Weg.
Aber dieser Code enthält zwei entscheidende Fehler,
einmal logisch und zum anderen syntaktisch.
So würde das auch nicht funktionieren!
Nicolas S. schrieb:> Nur habe ich den Overhead durch die Funktionszeiger.
Der ist vernachlässigbar gegenüber der Sicherung und Wiederherstellung
aller Register, die bei jedem Funktionsaufruf aus der ISR heraus gemacht
wird.
Bananen Joe schrieb:> Und woher weist du, dass deine von der ISR aufgerufenen Funktionen in> der Zeit fertig werden und dass du nicht die nächste Funktion aufrufst> während die vorherige noch läuft?
das weiss man ziemlich genau, weil nur eine ISR gleichzeitig laufen
kann. (bei einem Atmel ohne selber an dem flags rumzuschrauben)
> Deshalb ruft man aus Interruptroutinen keine Funktionen auf!
nein, das ist nicht der Grund. Der Grund ist das dabei der Overhead sehr
gross wird weil alle Register gesichert werden.
Nicolas S. schrieb:> Nur habe ich den Overhead durch die> Funktionszeiger.
Das ist natürlich absolut inakzeptabel ;)
Ernsthaft, über wieviel Takte "overhead" redest du da? Und lohnt es
sich, sich darüber überhaupt Gedanken zu machen?
Oliver
Uwe S. schrieb:> So würde das auch nicht funktionieren!
Stimmt, das Beispiel ist etwas übervereinfacht. Es funktioniert so:
1
typedefvoid(*voidFnct_t)(void);
2
voidFnct_tgl_isrfcns[6];
3
4
ISR(TIMER0_OVF_vect)// Takt 1kHz
5
{
6
voidFnct_t*ptr=gl_isrfcns;
7
while(*ptr!=NULL){
8
voidFnct_tfkt=*ptr;
9
ptr++;
10
fkt();
11
}
12
}
13
14
[...]
15
16
intmain(void){
17
gl_isrfcns[0]=encoder_poll;
18
gl_isrfcns[1]=glcd_timer;
19
gl_isrfcns[2]=NULL;
20
21
[...]
22
}
Syliosha schrieb:> Nebenbei ist der Overhead beim nutzen von Funktionspointern gegenüber> Funktionsaufrufen. Beide sind identisch.
Der Aufwand wird schon etwas größer sein, da er hier die Funktionen
nicht mehr inlinen kann. Aber wie groß der Mehraufwand ist, kann ich
momentan nicht abschätzen, weil gleichzeitig auch die *.lss-Datei
deutlich unübersichtlicher wird.
Oliver schrieb:> Ernsthaft, über wieviel Takte "overhead" redest du da? Und lohnt es> sich, sich darüber überhaupt Gedanken zu machen?
Das ist die Frage, die ich klären will.
Viele Grüße
Nicolas
Mit Funktionszeigerspielen: 12568 bytes (Prog mem) 1238 bytes (Data mem)
Mit Funktionsaufrufen: 12516 bytes (Prog mem) 1238 bytes (Data mem)
Kann ich da mit 26 Befehlen Unterschied je ISR-Aufruf rechnen?
Rolf Magnus schrieb:> Der ist vernachlässigbar gegenüber der Sicherung und Wiederherstellung> aller Register, die bei jedem Funktionsaufruf aus der ISR heraus gemacht> wird.
Was verleitet Dich zur Annahme, daß ein Funktionsaufruf aus einer ISR
heraus etwas anderes wäre als ein Funktionsaufruf außerhalb einer ISR?
Register etc. werden bei Aufruf der ISR gesichert, und beim Beenden der
ISR wiederhergestellt.
Sonst aber nicht; wozu auch?
Nicolas S. schrieb:> Kann ich da mit 26 Befehlen Unterschied je ISR-Aufruf rechnen?
Nein.
Es gibt nur eine Möglichekeit, das rauszufinden: Assemblercode ansehen,
und nachzählen.
Wenn du das nicht kannst, dann brauchst du das auch nicht. Vergiß die
paar Zyklen, und kümemr dich um wichtigeres.
Oliver
Gehen wir von einer F_CPU von 16 MHz aus, dann wird die ISR in Abständen
von 16000 Ticks aufgerufen.
Bei einem ISR-Overhead von 100 Ticks durch indirekte Aufrufe wären das
rund 0.6 % der Zeit, die der ISR verbleiben...
Rufus Τ. Firefly schrieb:> Was verleitet Dich zur Annahme, daß ein Funktionsaufruf aus einer ISR> heraus etwas anderes wäre als ein Funktionsaufruf außerhalb einer ISR?
Darum geht's ja nicht.
Wenn man über einen Funktionszeiger aufruft, dann muss der Compiler
grundsätzlich alle Register sichern, die eine x-beliebige gerufene
Funktion gemäß ABI zerstören darf.
Wenn man die Funktion direkt aufruft, kann der Compiler ggf. direkt
feststellen, welche Register tatsächlich gesichert werden müssen.
Falls die gerufenen Funktionen hinreichend komplex sind, ist das aber
egal. Falls sie aber nur an einem Pin wackeln oder sowas, kann der
Unterschied erheblich sein.
Jörg Wunsch schrieb:> Wenn man die Funktion direkt aufruft, kann der Compiler ggf. direkt> feststellen, welche Register tatsächlich gesichert werden müssen.
kann er eigentlich nie. Nur wenn er sie inlined, dann wird sie aber auch
nicht mehr als funktion aufrufen.
Rufus Τ. Firefly schrieb:> Rolf Magnus schrieb:>> Der ist vernachlässigbar gegenüber der Sicherung und Wiederherstellung>> aller Register, die bei jedem Funktionsaufruf aus der ISR heraus gemacht>> wird.>> Was verleitet Dich zur Annahme, daß ein Funktionsaufruf aus einer ISR> heraus etwas anderes wäre als ein Funktionsaufruf außerhalb einer ISR?
Vor allem die Tatsache, daß ich es im generierten Assembler-Code gesehen
habe. Hier mal ein ganz einfaches Beispiel:
1
#include<avr/interrupt.h>
2
3
voidfoo()
4
{
5
}
6
7
void(*fun)()=foo;
8
9
intmain()
10
{
11
fun();
12
}
13
14
ISR(INT0_vect)
15
{
16
fun();
17
}
18
19
ISR(INT1_vect)
20
{
21
}
übersetzt mit avr-gcc isr_test.c -mmcu=atmega8 -c -S -o- -O3
Aus main() wird dabei folgendes:
1
lds r30,fun
2
lds r31,fun+1
3
icall
4
ret
Aus INT1_vect (also ohne Aufruf der Funktion) folgendes:
1
push r1
2
push r0
3
in r0,__SREG__
4
push r0
5
clr __zero_reg__
6
/* prologue: Signal */
7
/* frame size = 0 */
8
/* stack size = 3 */
9
.L__stack_usage = 3
10
/* epilogue start */
11
pop r0
12
out __SREG__,r0
13
pop r0
14
pop r1
15
reti
und aus INT0_vect das hier:
1
push r1
2
push r0
3
in r0,__SREG__
4
push r0
5
clr __zero_reg__
6
push r18
7
push r19
8
push r20
9
push r21
10
push r22
11
push r23
12
push r24
13
push r25
14
push r26
15
push r27
16
push r30
17
push r31
18
/* prologue: Signal */
19
/* frame size = 0 */
20
/* stack size = 15 */
21
.L__stack_usage = 15
22
lds r30,fun
23
lds r31,fun+1
24
icall
25
/* epilogue start */
26
pop r31
27
pop r30
28
pop r27
29
pop r26
30
pop r25
31
pop r24
32
pop r23
33
pop r22
34
pop r21
35
pop r20
36
pop r19
37
pop r18
38
pop r0
39
out __SREG__,r0
40
pop r0
41
pop r1
42
reti
> Register etc. werden bei Aufruf der ISR gesichert, und beim Beenden der> ISR wiederhergestellt.
Ja, aber nur genau die, die die ISR für sich selbst braucht - es sei
denn, eine Funktion wird aufgerufen. Dann ist unbekannt, welche Register
von der Funktion geändert werden.
Rolf Magnus schrieb:> Ja, aber nur genau die, die die ISR für sich selbst braucht - es sei> denn, eine Funktion wird aufgerufen. Dann ist unbekannt, welche Register> von der Funktion geändert werden.
Mit meinen bescheidenen Programmierkenntnissen würde ich das sogar noch
etwas einschränken [Hoffe, ich erinnere mich richtig]: Die
'push-pop-Orgie' wird dann eingefügt, wenn sich die Funktion in einem
anderen Modul befindet oder, wie oben, über einen Zeiger aufgerufen
wird. Eine Funktion innerhalb der gleichen Datei wird vom GCC
entsprechend durchleuchtet (evtl. auch ge-inlined) und in der ISR werden
nur die wirklich benötigten Register gerettet.
Ralf G. schrieb:> Eine Funktion innerhalb der gleichen Datei wird vom GCC> entsprechend durchleuchtet (evtl. auch ge-inlined) und in der ISR werden> nur die wirklich benötigten Register gerettet.
wenn sie ge-inlined dann ja, sonst meines wissens nicht. Denn dafür
müsste sich der compiler von jeder funtion merken welche Register sie
verwendet.
Peter II schrieb:> Denn dafür> müsste sich der compiler von jeder funtion merken welche Register sie> verwendet.
Geh mal davon aus, daß der sich noch sehr viel mehr merkt...
Oliver
Oliver schrieb:> Geh mal davon aus, daß der sich noch sehr viel mehr merkt...
dann versteckt er das verhalten so gut, das ich es noch nicht beobachten
konnte. Jeder Funktionsaufruf führe dazu das alle Register gesichert
wurden.
@ Peter II (Gast)
>dann versteckt er das verhalten so gut, das ich es noch nicht beobachten>konnte. Jeder Funktionsaufruf führe dazu das alle Register gesichert>wurden.
Na, es sind viele, aber nicht alle 32.
Oliver schrieb:> Peter II schrieb:>> Denn dafür müsste sich der compiler von jeder funtion>> merken welche Register sie verwendet.>> Geh mal davon aus, daß der sich noch sehr viel mehr merkt...
Das stimmt zwar, aber die o.g. Optimierung wird für Funktionen nicht
ausgeführt, d.h. auch wenn die Implementierung der Funktion bekannt ist,
wird dieses Wissen im Caller nicht dazu verwendet, die Registerlast zu
verkleinern.
Ausnahmen sind einige Funktionen in der libgcc, die in Assembler
implementiert sind und deren Register-Fußabdruck dem Compiler explizit
eingehämmert wurden.
Ansonsten gilt: Eine Funktion, die (im compilierten Code) aufgerufen
wird, wird wie eine Black Box behandelt, d.h. alle call-clobbered
Register sind als zerstört anzusehen.
Was der Compiler machen kann ist Inlining (kein Blach Box-Overhead) oder
partielles Inlining oder Function Cloning (Prototyp wird an den Call
angepasst). In den beiden letzten Fällen ensteht der Black Box Overhead
durch den verbleibenden Call.
Ob der Call nun direkt oder indirekt ausgeführt wird ist ziemlich
Wurscht, nur ist bei einem direkten Aufruf die Wahrscheinlichkeit
wesentlich größer, daß der Compiler erkennt, wie der Code des Callie
denn genau lautet.
Warum die o.g. Optimierung von GCC nicht durchgeführt wird — da müsst
ihr die GCC-Entwickler fragen. Wahrscheinlich wie bei so vielen anderen
Dingen auch: Es ist nicht implementiert, weil es bislang niemandem
wichtig genug war, es einzubauen.
Das Sichern der Register scheint tatsächlich nur bei geinlineten
Funktionsaufrufen optimiert zu werden. Ok, Johann hat die Ausnahmen
dieser Regel genannt.
C:
1
#include<stdint.h>
2
#include<avr/interrupt.h>
3
4
volatileuint8_tdummy;
5
6
voidfoo()
7
{
8
dummy=1;
9
}
10
11
voidfoo_noinline()__attribute__((noinline));
12
voidfoo_noinline()
13
{
14
dummy=1;
15
}
16
17
ISR(INT0_vect)
18
{
19
foo();
20
}
21
22
ISR(INT1_vect)
23
{
24
foo_noinline();
25
}
Daraus von GCC 4.8.1 mit -Os generierter Assembler-Code:
Johann L. schrieb:> Warum die o.g. Optimierung von GCC nicht durchgeführt wird — da müsst> ihr die GCC-Entwickler fragen.
Ich könnte mir vorstellen, dass der GCC bei einer externen Funktion
grundsätzlich erstmal davon ausgeht, dass diese ggf. durch den Linker
ja noch ersetzt werden könnte. Regulär geht das natürlich nur, wenn
sie als “weak” deklariert ist. Vielleicht lässt sich die Wirkung
dieses Attributs (das ja eigentlich sonst nur dem Linker durchgereicht
werden muss) auch im Tree intern nicht mehr abbilden, sodass er an
dieser Stelle einfach nicht weiß, ob eine externe Funktion weak ist
oder nicht.
@Johann L. (gjlayde) Benutzerseite
>Ansonsten gilt: Eine Funktion, die (im compilierten Code) aufgerufen>wird, wird wie eine Black Box behandelt, d.h. alle call-clobbered>Register sind als zerstört anzusehen.
Woher stammt eigentlich dieses seltsame Verhalten, dass VOR dem Aufruf
von Funktionen die Register vorsorglich gesichert werden?
Wäre es denn nicht sinnvoller, die Sicherung der "beschmutzten" Register
in die Funktion zu verlegen? Da entfällt nämlich auch das Problem der
unnötigen Sicherung der Register in der ISR.
Schon klar, die Leute vom GCC werden sich schon was dabei gedacht haben,
die Frage ist WAS?
Falk Brunner schrieb:> Woher stammt eigentlich dieses seltsame Verhalten, dass VOR dem Aufruf> von Funktionen die Register vorsorglich gesichert werden?
werden sie ja nicht. Die ISR sicher ihre register die sie verwendet. Da
sie nicht weiss welche sie verwendet sicher sie alle.
Nach deiner logic müsste man ja gar keine register sichern, wenn man
keine funktion aufruft.
Falk Brunner schrieb:> Woher stammt eigentlich dieses seltsame Verhalten, dass VOR dem Aufruf> von Funktionen die Register vorsorglich gesichert werden?
Siehe ABI-Beschreibung (in der avr-libc-FAQ).
Es gibt halt Register, die sind “caller-saved”, und welche, die sind
“callee-saved”. Erstgenannte müssen in einer ISR halt extra
gesichert werden, bevor eine Funktion gerufen werden kann.
Die Festlegung des ABIs ist reichlich alt (weshalb dummerweise auch
R0 und R1 darin eine Sonderrolle bekommen haben; sowas wie die
Multiplikationsbefehle gab es damals noch nicht). Schätzungsweise
ist das immer eine Abwägung zwischen dem, dass der Aufgerufene sowieso
Arbeitsregister braucht (die Parameter fallen da am Ende auch mit rein),
und den auf den Stack zu sichernden Registern. Wenn
du gar keine “caller-saved”-Register einrichtest, kann es gut sein,
dass der Aufgerufene Dinge auf den Stack sichert, die anschließend
vom Aufrufer weggeworfen werden, weil er sie gleich wieder als
Arbeitsregister überschreibt.
Falk Brunner schrieb:> Woher stammt eigentlich dieses seltsame Verhalten, dass VOR dem Aufruf> von Funktionen die Register vorsorglich gesichert werden?
Grund ist das ABI und dort das Register-Layout:
http://gcc.gnu.org/wiki/avr-gcc#Register_Layout> Schon klar, die Leute vom GCC werden sich schon was dabei gedacht haben,> die Frage ist WAS?
Das ABI wiederum enstand als Co-Entwicklung zusammen mit dem AVR-Backend
des GCC, damals (2000) durch Denis Chertykov. Dabei hat der ziemlich
viel mit unterschiedlichen Layouts rumexperimentiert — auch in welchen
Registern Werte übergeben werden. Das dabei gemachten Erkenntnisse
flossen ins ABI ein, das bis heute i.W. unverändert ist. Auch die
Entscheidung, R0 als Scratch-Register zu verwenden und R1 als
Zero-Register datiert in diese Zeit, zu der es — wie Jörg bereits
bemerkte — noch keine MUL-Befehle mit den impliziten Registern R0 und R1
gab.
> Wäre es denn nicht sinnvoller, die Sicherung der "beschmutzten" Register> in die Funktion zu verlegen? Da entfällt nämlich auch das Problem der> unnötigen Sicherung der Register in der ISR.
Ein Jeder-kümmert-sich-um-seinen-eigenen-Scheiß-ABI also...
Das würde bedeuten, daß es keine Call-Clobbered Register gibt und jede
Funktion alle Register sichert, die sie anfasst. Das würde z.B.
Leaf-Funktionen viel teurer machen. Diese erzeugen beim momentanen ABI
nur dann PUSH/POP, wenn das Kontingent an Call-Clobbered Registern
ausgeschöpft ist. Für den Code von oben würde das lediglich bedeuten,
daß sich die PUSH/POP-Orgie in den Callee verlagern würde — und damit
bei jedem Aufruf (Laufzeit-)Overhead erzeugt, nicht nur einmalig beim
Betreten/Verlassen der ISR.
Wenn du damit rumspielen magst, die Definition ist in
CALL_USED_REGISTERS und überschreibbar in
TARGET_CONDITIONAL_REGISTER_USAGE:
http://gcc.gnu.org/onlinedocs/gccint/Register-Basics.html#Register-Basics
Implementiert in avr.h bzw. avr.c:
http://gcc.gnu.org/viewcvs/gcc/trunk/gcc/config/avr/avr.h?view=markup#l171http://gcc.gnu.org/viewcvs/gcc/trunk/gcc/config/avr/avr.c?view=markup
Allerdings ist nicht damit zu rechnen, daß nach Änderung korrekter Code
erzeugt wird, denn die in Assembler stehenden Funktionen der libgcc
und AVR-Libc kümmern sich natürlich nicht darum :-P
Weiters gibt es CALLER_SAVE_PROFITABLE:
http://gcc.gnu.org/onlinedocs/gccint/Caller-Saves.html#Caller-Saves
das im AVR-Backend nicht überschrieben wird. Allerdings liest man da:
>> [...] to determine whether it is worthwhile to consider placing>> a pseudo-register in a call-clobbered hard register and saving>> and restoring it around each function call. [...]
Das Register muß dazu auf dem Stack in einem eigenen Stackslot gesichert
werden, was teuer ist. Es in einem Call-Saved Register zu sichern wäre
witzlos, denn dann könnte es direkt dort allokiert werden.
Meine Erfahrung mit diesem Caller-Saves ist nicht gut. Falls es
verwendet wird, liefert es nicht selten schlechteren Code, d.h. der Code
wird dann mit -fno-caller-saves besser.
Puhhh, da hab ich ja mal wieder ne Frage gestellt!
Danke für die Antworten, aber in irgendwelchen Compilern rumschrauben
ist nun Weiß Gott nicht mein Ding. Bin doch nur ein kleiner Hardwerker
;-)
Der avr gcc ist im Wesentlichen schon recht gut, nur in einigen Fällen
produziert er halt etwas merkwürdigen Code, den der gelernte
Assemblerprogrammierer mit seinem Tunnelblick für die optimal angepasste
Lösung nicht versteht.
Mit etwas Distanz versteht aber sogar jemand wie ich, dass auch der
beste Compiler ein Kompromiss ist aus generischen Programmustern und
Konventionen und speziellen Optimierungen.
Wenn's halt WIRKLICH schnell sein muss, nimmt man halt ne komplette ISR
in reinem Assembler. Oder einen 32 Bit ARM. Oder noch besser, ein FPGA.
;-)