2013-05-11 7 views
13

Betrachten Sie den folgenden Code ein:Warum ist die Inlined-Funktion langsamer als der Funktionszeiger?

typedef void (*Fn)(); 

volatile long sum = 0; 

inline void accu() { 
    sum+=4; 
} 

static const Fn map[4] = {&accu, &accu, &accu, &accu}; 

int main(int argc, char** argv) { 
    static const long N = 10000000L; 
    if (argc == 1) 
    { 
      for (long i = 0; i < N; i++) 
      { 
        accu(); 
        accu(); 
        accu(); 
        accu(); 
      } 
    } 
    else 
    { 
      for (long i = 0; i < N; i++) 
      { 
        for (int j = 0; j < 4; j++) 
          (*map[j])(); 
      } 
    } 
} 

Als ich es kompiliert mit:

g++ -O3 test.cpp 

Ich bin der erste Zweig erwartet, schneller zu laufen, da der Compiler den Funktionsaufruf zu accu Inline könnte. Und der zweite Zweig kann nicht inline sein, weil der Akku über den in einem Array gespeicherten Funktionszeiger aufgerufen wird.

Aber die Ergebnisse überrascht mich:

time ./a.out 

real 0m0.108s 
user 0m0.104s 
sys 0m0.000s 

time ./a.out 1 

real 0m0.095s 
user 0m0.088s 
sys 0m0.004s 

Ich verstehe nicht, warum, so habe ich eine objdump:

objdump -DStTrR a.out > a.s 

und die Demontage scheint nicht das Leistungsergebnis zu erklären I bekam:

8048300 <main>: 
8048300:  55      push %ebp 
8048301:  89 e5     mov %esp,%ebp 
8048303:  53      push %ebx 
8048304:  bb 80 96 98 00   mov $0x989680,%ebx 
8048309:  83 e4 f0    and $0xfffffff0,%esp 
804830c:  83 7d 08 01    cmpl $0x1,0x8(%ebp) 
8048310:  74 27     je  8048339 <main+0x39> 
8048312:  8d b6 00 00 00 00  lea 0x0(%esi),%esi 
8048318:  e8 23 01 00 00   call 8048440 <_Z4accuv> 
804831d:  e8 1e 01 00 00   call 8048440 <_Z4accuv> 
8048322:  e8 19 01 00 00   call 8048440 <_Z4accuv> 
8048327:  e8 14 01 00 00   call 8048440 <_Z4accuv> 
804832c:  83 eb 01    sub $0x1,%ebx 
804832f:  90      nop 
8048330:  75 e6     jne 8048318 <main+0x18> 
8048332:  31 c0     xor %eax,%eax 
8048334:  8b 5d fc    mov -0x4(%ebp),%ebx 
8048337:  c9      leave 
8048338:  c3      ret 
8048339:  b8 80 96 98 00   mov $0x989680,%eax 
804833e:  66 90     xchg %ax,%ax 
8048340:  8b 15 18 a0 04 08  mov 0x804a018,%edx 
8048346:  83 c2 04    add $0x4,%edx 
8048349:  89 15 18 a0 04 08  mov %edx,0x804a018 
804834f:  8b 15 18 a0 04 08  mov 0x804a018,%edx 
8048355:  83 c2 04    add $0x4,%edx 
8048358:  89 15 18 a0 04 08  mov %edx,0x804a018 
804835e:  8b 15 18 a0 04 08  mov 0x804a018,%edx 
8048364:  83 c2 04    add $0x4,%edx 
8048367:  89 15 18 a0 04 08  mov %edx,0x804a018 
804836d:  8b 15 18 a0 04 08  mov 0x804a018,%edx 
8048373:  83 c2 04    add $0x4,%edx 
8048376:  83 e8 01    sub $0x1,%eax 
8048379:  89 15 18 a0 04 08  mov %edx,0x804a018 
804837f:  75 bf     jne 8048340 <main+0x40> 
8048381:  eb af     jmp 8048332 <main+0x32> 
8048383:  90      nop 
... 
8048440 <_Z4accuv>: 
8048440:  a1 18 a0 04 08   mov 0x804a018,%eax 
8048445:  83 c0 04    add $0x4,%eax 
8048448:  a3 18 a0 04 08   mov %eax,0x804a018 
804844d:  c3      ret 
804844e:  90      nop 
804844f:  90      nop 

Es scheint der direkte Anruf Zweig ist definitiv weniger als die Funktion Poin ter Zweig. Aber warum läuft der Funktionszeigerzweig schneller als der direkte Aufruf?

Und beachten Sie, dass ich nur "Zeit" für die Messung der Zeit verwendet habe. Ich habe clock_gettime verwendet, um die Messung durchzuführen, und ähnliche Ergebnisse erhalten.

+0

für Format Es ist leider ‚root @ ubuntu:/cm/gt # g ++ haha. CPP -o za.exx -O3 root @ ubuntu:/cm/gt # Zeit ./za.exx reale \t 0m0.092s Benutzer \t 0m0.084s sys \t 0m0.004s root @ ubuntu:/cm/gt # Zeit./za.exx 1 echte \t 0m0.146s Benutzer \t 0m0.072s sys \t 0m0.000s' –

+0

Beide Codes auf gcc in der gleichen Zeit laufen 4.7. – mfontanini

Antwort

6

Es stimmt nicht ganz, dass der zweite Zweig nicht inline sein kann. Tatsächlich werden alle im Array gespeicherten Funktionszeiger zur Kompilierzeit gesehen. So kann der Compiler indirekte Funktionsaufrufe durch direkte Aufrufe ersetzen (und tut dies auch). In der Theorie kann es weiter gehen und inline (und in diesem Fall haben wir zwei identische Zweige). Aber dieser spezielle Compiler ist nicht intelligent genug, um dies zu tun.

Als Ergebnis wird der erste Zweig "besser" optimiert. Aber mit einer Ausnahme. Der Compiler darf die flüchtige Variable sum nicht optimieren. Wie Sie aus zerlegten Code sehen können, dieser Speicher erzeugt Anweisungen sofort durch Ladeanweisungen gefolgt (in Abhängigkeit von diesen Speicherbefehle):

mov %edx,0x804a018 
mov 0x804a018,%edx 

Intel Bedienungsanleitung Software Optimization (Abschnitt 3.6.5.2) keine Anweisungen, wie dies empfiehlt Anordnung:

... Wenn eine Ladung zu früh nach dem Laden geplant wird, hängt es davon ab, ob die Generierung der zu speichernden Daten verzögert wird, kann es zu einer erheblichen Strafe kommen.

Die zweite Verzweigung vermeidet dieses Problem aufgrund zusätzlicher Aufruf-/Rückgabeanweisungen zwischen Laden und Laden. Es funktioniert also besser.

ähnliche Verbesserungen können für den ersten Zweig erfolgen, wenn wir ein paar (nicht sehr teuer) Berechnungen in-between hinzufügen:

long x1 = 0; 
for (long i = 0; i < N; i++) 
{ 
    x1 ^= i<<8; 
    accu(); 
    x1 ^= i<<1; 
    accu(); 
    x1 ^= i<<2; 
    accu(); 
    x1 ^= i<<4; 
    accu(); 
} 
sum += x1; 
+2

Dieser Effekt kann auch verifiziert werden, indem die Dinge im Beispielprogramm ein wenig geändert werden, so dass es 4 separate flüchtige Variablen und 4 separate Inline-Funktionen gibt. Die Inlined-Version benötigt die Hälfte der Zeit der Nicht-Inline-Version. –

+1

Ich denke, die Meinung des Compiler-Schreibers ist, dass entweder die Variable nicht flüchtig ist und sie nicht sofort speichert und liest, oder die Variable flüchtig ist und der Benutzer danach fragt, so dass sie keine Optimierung anstreben. –

+0

Der Grund, warum ich die volatile Variable verwendet habe, besteht darin, zu verhindern, dass der Compiler die gesamte Funktion entfernt. – Ming