Hallo,
ich bin da auf ein komisches problem gestoßen.
zähle ich eine variable in einer funktion hoch, dauert das fast eine
sekunde länger. Kann ich mir nicht erklären...
der schnelle code:
Schau dir mal die beiden ASM Listings
zu deinen Codes an. Wenn du seek in die
Funktion legst könnte der Compiler zusätzliche
Pushs und Pops am Anfang und am Ende einfügen.
Dann dauert die Funktion länger.
Ok,
durch das hochzählen von einem "unsigned long int" wird die funktion
wesentlich länger in asm.
Ich öffne eine datei mit 629461 bytes zum lesen, zähle ich die glesenen
bytes in der lesen funktion braucht es 4,2117 sekunden,
zähle ich die gelesenen bytes auserhalb der lesen funktion 3,4347
sekunden, das sind mehrere kbytes/sec langsamer...
echt mies !
Ich glaub gar nicht mal so sehr, dass das Problem im Hochzählen
besteht.
Meine Überlegung ist diese:
Hast du die Erhöhung ausserhalb, zb so
1
voidfoo()
2
{
3
unsignedlonginti=0;
4
5
...
6
7
while(i<file.length){//bis zum letzten byte
8
c=ffread();
9
i++;
10
}
11
}
dann kann der Compiler das i in einem Register halten. Dadurch
dass i lokal zu foo() ist, ist auch ausgeschlossen, dass es zu
Aliasing kommt, sprich das i braucht beim ++ nicht erneut aus
dem Speicher gelesen bzw. nach der Erhöhung in den Speicher
geschrieben werden. Die Variable i kann komplett in einem
Register residieren.
Hast du aber die andere Variante, wobei seek offenbar eine
globale Variable ist, dann bleibt dem Compiler gar nichts anderes
übrig, als den Lese/Schreibzugriff bei seek++ tatsächlich
durchzuführen, bzw. hier
1
while(seek<file.length)
auch einen tatsächlichen Speicherzugriff zu machen.
Was mich allerdings auch erstaunt, ist die Höhe des Penalties.
1 Sekunde bei 4 Sekunden Komplettlaufzeit, ist mir dann doch zu
heftig, als das ich das glauben könnte. Vor allem wenn man bedenkt
wie wenig Zeit diese Operation im Vergleich zum Rest benötigen
wird. Da ist noch irgendwas anderes faul.
Hm,
so ist das wohl,
weil wenn ich nun i als volatile deklariere,
hab ich fast die gleiche ausführungszeit wie bei der anderen variante
mit seek.
danke für die erklärung !
muss mir dann mal was anderes überlegen wie ich das mach...
grüße daniel
>Was mich allerdings auch erstaunt, ist die Höhe des Penalties.
Warum ? Die zusätzlichen ASM Zeilen in der Funktion multiplizieren sich
mit file.length. Ausserdem baut der Compiler durchaus
einen ungünstigeren Code wenn man etwas hinzufügt.
holger wrote:
>>Was mich allerdings auch erstaunt, ist die Höhe des Penalties.>> Warum ?
Weil in seiner Funktion noch Aufrufe von weiteren Funktionen
vorkommen, die nicht danach klingen als ob sie in ein paar
Taktzyklen durchlaufen würden.
Das die Nicht-Register Variante ein paar Prozente langsamer
ist, ok. Aber gleich 25%. Das macht mich stutzig.
Damit hast Du ganz elegent schwer auffindbare Bugs vorprogrammiert! Der
Grund dafür steht in der von Dir zitierten Passage anbei.
Für long int etwa darf keines der so verwendeten Register (immerhin 8
Stück, nähmlich r8-r15) in einer anderen Routine (auch in keiner von dir
verwendeten Bibliotheksroutine (libc, libm, libgcc(!), ...) verwendet
werden.
In Deinem gesamten Projekt muss jede Quelle davon wissen, auch solche,
die seek nicht verwenden!
Last not least blockierst Du damit n Haufen GPRs, was die
Wahrscheinlichkeit, daß gcc für eine Funktion einen Frame anlegt,
deutlich erhöht -- insbesondere für Funktionen, die keine Blätter
(leafs) sind. Der Geschwindigkeitsgewinn wird also an anderer Stelle
verpuffen...
>Weil in seiner Funktion noch Aufrufe von weiteren Funktionen>vorkommen, die nicht danach klingen als ob sie in ein paar>Taktzyklen durchlaufen würden.
Die werden aber nicht unbedingt bei jedem Durchlauf aufgerufen ;)
if(sec_cntByte==512){
if(ccPointer==9) bufferClusterChain(clusterChain[9]);
...
}
sec_cntByte++;
holger wrote:
>>Weil in seiner Funktion noch Aufrufe von weiteren Funktionen>>vorkommen, die nicht danach klingen als ob sie in ein paar>>Taktzyklen durchlaufen würden.>> Die werden aber nicht unbedingt bei jedem Durchlauf aufgerufen ;)>> if(sec_cntByte==512){> if(ccPointer==9) bufferClusterChain(clusterChain[9]);> ...> }> sec_cntByte++;
schon richtig. Aber immerhin ziemlich oft:
Filesize = 629461
629461 / 512 = 1329 mal für fat_loadSector (und der Funktionsname
klingt nicht unbedingt nach wenig
Arbeit
OK. die 140 Aufrufe von bufferClusterChain laufen wahrscheinlich
unter 'ferner liefen'.
@ Georg-johann L. (sprintersb)
versteh ich da was falsch, in assembler bedient man sich doch öfter
solcher register um variablen zu ändern oder sonst was, weil die teils
mit 1. takt abgearbeitet werden können....
und ist mit dem namen "r8" auch die adresse 0x08 gemeint? (siehe
datenblatt atmega168 seite:335 (register summary)), weil wenn ja, dann
hab ich damit portd und portc komplett lahmgelegt.
Müsste der compiler nicht meckern wenn ich ne lib benutze und
gleichzeitig register die diese benutzt, auch benutze ? wenn nein,
sollter er das aber!!
@ Karl heinz Buchegger und holger
leider kann man das ja kaum verkürzen, das nachladen, vielleicht mit
mehr speicher zum puffern der fat, aber sonst seh ich da keine
möglichkeit (spart in diesem fall dann 137 aufrufe von
bufferClusterChain)...
Daniel Platte wrote:
> @ Georg-johann L. (sprintersb)>> versteh ich da was falsch, in assembler bedient man sich doch öfter> solcher register um variablen zu ändern oder sonst was, weil die teils> mit 1. takt abgearbeitet werden können....
Ja, allerdings bestimmt der Compiler für sich, welche Variable er zu
welchem Zeitpunkt in einem Register hält.
> und ist mit dem namen "r8" auch die adresse 0x08 gemeint? (siehe> datenblatt atmega168 seite:335 (register summary)), weil wenn ja, dann> hab ich damit portd und portc komplett lahmgelegt.> Müsste der compiler nicht meckern wenn ich ne lib benutze und> gleichzeitig register die diese benutzt, auch benutze ? wenn nein,> sollter er das aber!!
r8 heißt Register 8 und bedeutet auch dass Register Nummer 8 (von 32
möglichen)
Daniel Platte wrote:
> @ Georg-johann L. (sprintersb)>> versteh ich da was falsch, in assembler bedient man sich doch öfter> solcher register um variablen zu ändern oder sonst was, weil die teils> mit 1. takt abgearbeitet werden können....> und ist mit dem namen "r8" auch die adresse 0x08 gemeint? (siehe> datenblatt atmega168 seite:335 (register summary)), weil wenn ja, dann> hab ich damit portd und portc komplett lahmgelegt.
asm ("r8") bezeichnet mehrere Register, abhängig davon, welchen Typ man
speichern will. Für long sind das 4 aufeinander folgende GPRs. (Ich
hatte oben was von 8 geschrieben, das wäre der Fall für long long).
SRAM Adresse 8 ist Register r8, es gibt aber noch die Adresse 8 im
I/O-Bereich, eine Adresse 8 im Flash und eine im EEPROM, eben wegen der
Harvard-Architektur des AVR. D.h. man hat keinen linearen Adressraum,
also die verschiedenen 8s nicht verwechseln.
> Müsste der compiler nicht meckern wenn ich ne lib benutze und> gleichzeitig register die diese benutzt, auch benutze ? wenn nein,> sollter er das aber!!
Zunächst mal sieht der Compiler nicht, welche Libs Du verwendest. Er
sieht Dein Programm und Prototypen irgendwelcher Funktionen/Variablen.
Er kann nicht wissen, was darin abgeht, und braucht es auch nicht. Der
GCC hat eine definierte Registerverwendung, damit zB die Kommunikation
mit Bibliotheken, die unter der gleichen ABI (application binary
interface) generiert wurden, klappt.
Du könntest also hingehen und alle die Bibliotheken, die Du verwendest,
neu generieren und ihm sagen, daß es die angedachten Register nicht
verwenden soll. Allerdings brauchen manche Libs (zB float, also libm)
recht viele GPRs und das Neugenerieren würde wohl nicht durchlaufen.
Der Schalter dazu wäre -ffixed-<regno>, aber das willst Du nicht
wirklich...
Der ld (Linker) wiederum kümmert sich nicht um Registerverwendung, das
ist Aufgabe vom gcc. Er schaut nur, welche Symbole da sind und
lokatiert, d.h. füllt Platzhalter aus (z.B. Variablen und
Funktionsadressen).
Zwischen gcc und ld kommt noch as (Assembler) zum Zuge. Auch er ist
nicht in der Lage, Register zu allokieren. Er übersetzt einfach .s nach
.o, d.h. erstellt Symboltabellen und Maschinencode mit Platzhaltern
(Fuxups), die der Linker irgendwann mal ausfüllt und Debug-Info aus
einer Datei, die gcc irgendwann mal erstellte.
Wenn es wirklich zeitkritisch ist was Du machst, kannst Du mit einer
Implementierung anfangen, die einen (relativ) guten Code liefert wie
1
externunsignedlongbar(void);
2
externunsignedlongfile_size;
3
4
voidfoo(void)
5
{
6
unsignedlongsize;
7
8
for(size=file_size;size;size--)
9
{
10
bar();
11
}
12
}
und die Schleife von Hand teilweise aufrollen:
1
voidfoo8(void)
2
{
3
unsignedlongsize=file_size;
4
unsignedcharsize8;
5
6
// size % 8 calls
7
for(size8=size&(8-1);size8;size8--)
8
{
9
bar();
10
}
11
12
// size - (size % 8) calls
13
for(size=size>>3;size;size--)
14
{
15
bar();bar();bar();bar();
16
bar();bar();bar();bar();
17
}
18
}
gcc kann zwar Schleifen aufrollen mit -funroll-loops oder
-funroll-all-loops, was aber nicht wiklich zu überzeugendem Code führt.
Hallo,
äh das muss ich erstmal etwas sacken lassen.
Schleifen aufrollen? Ist mir momantan noch ein rätsel!
Die speicherverwaltung von den avr mit den verschiedenen bereichen,
kollidiert ja auch schonmal mit c an sich, welches da keine
unterscheidung kennt. Dann noch die aufteilung in blätter usw, etwas
unübersichtlich...
So wie das bis jetzt für mich klingt ist meine variable da in dem
register nicht sicher, weil irgendwas sie einfach überschreiben könnte,
ohne das dass registriert wird.