Forum: Compiler & IDEs Ausführungszeit von C code, ist in funktionen langsamer?


von Daniel R. (zerrome)


Lesenswert?

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:
1
unsigned long int i=0;
2
3
ffopen('r',"AVR     PDF");
4
  
5
    
6
while(i<file.length){        //bis zum letzten byte        
7
  c=ffread();          
8
  i++;        
9
  }
10
11
12
die funktion:
13
14
unsigned char ffread(void){
15
  
16
   if(file_flag==3){    
17
    
18
  if(sec_cntByte==512){
19
     if(ccPointer==9) bufferClusterChain(clusterChain[9]);    
20
     if(0==fat_loadSector(clusterChain[ccPointer]+dataDirSec)) {  
21
        ccPointer++;
22
        sec_cntByte=0;              
23
        }        
24
     }
25
  sec_cntByte++;                          
26
  //seek++;                            
27
  return sector[sec_cntByte-1];            
28
   }
29
30
return(0x18);                            
31
}

der langsame code:
1
ffopen('r',"AVR     PDF");
2
    
3
while(seek<file.length){        //bis zum letzten byte        
4
  c=ffread();              
5
  }
6
7
8
die funktion:
9
10
unsigned char ffread(void){
11
  
12
   if(file_flag==3){    
13
    
14
  if(sec_cntByte==512){
15
     if(ccPointer==9) bufferClusterChain(clusterChain[9]);    
16
     if(0==fat_loadSector(clusterChain[ccPointer]+dataDirSec)) {  
17
        ccPointer++;
18
        sec_cntByte=0;              
19
        }        
20
     }
21
  sec_cntByte++;                          
22
  seek++;                            
23
  return sector[sec_cntByte-1];            
24
   }
25
26
return(0x18);                            
27
}

ob ich doch jetzt nach dem aufruf von ffread() ne variable hochzähle 
oder in der funktion selber ist doch wohl kein unterschied, oder?

von holger (Gast)


Lesenswert?

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.

von Daniel R. (zerrome)


Lesenswert?

Hm wie komme ich an die ASM Listings?

von Daniel R. (zerrome)


Lesenswert?

.lss dateien?

von Daniel R. (zerrome)


Lesenswert?

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 !

von Karl H. (kbuchegg)


Lesenswert?

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
void foo()
2
{
3
  unsigned long int i = 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.

von Daniel R. (zerrome)


Lesenswert?

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

von holger (Gast)


Lesenswert?

>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.

von Daniel R. (zerrome)


Lesenswert?

Hallo,

wen es interessiert,
es gibt die möglichkeit variablen fest an register zu binden.

z.b. für lange variablen
1
register unsigned long int seek asm("r8");

oder für kurze:
1
register unsigned char counter asm("r3");

siehe:

http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_regbind

habe nur die zähl variabel seek registriert und die schnellste lese 
geschwindigkeit bis jetzt :)

von Karl H. (kbuchegg)


Lesenswert?

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.

von G. L. (sprintersb)


Lesenswert?

Daniel Platte wrote:
>
1
> register unsigned long int seek asm("r8");
2
>

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...

von holger (Gast)


Lesenswert?

>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++;

von Karl H. (kbuchegg)


Lesenswert?

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'.

von Daniel R. (zerrome)


Lesenswert?

@  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)...

von Simon K. (simon) Benutzerseite


Lesenswert?

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)

von G. L. (sprintersb)


Lesenswert?

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
extern unsigned long bar (void);
2
extern unsigned long file_size;
3
4
void foo (void)
5
{
6
    unsigned long size;
7
    
8
    for (size = file_size; size; size--)
9
    {
10
        bar();
11
    }    
12
}

und die Schleife von Hand teilweise aufrollen:
1
void foo8 (void)
2
{
3
    unsigned long size = file_size;
4
    unsigned char size8;
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.

von Daniel R. (zerrome)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.