2013-05-11 10 views
13

Si consideri il seguente codice:perché la funzione in linea è più lenta del puntatore di funzione?

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])(); 
      } 
    } 
} 

Quando ho compilato con:

g++ -O3 test.cpp 

mi aspetto il primo ramo a correre più veloce perché il compilatore potrebbe inline la chiamata di funzione per accu. E il secondo ramo non può essere in linea perché accu è chiamato attraverso il puntatore di funzione memorizzato in un array.

ma i risultati mi hanno sorpreso:

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 

Non capisco il motivo per cui, così ho fatto un objdump:

objdump -DStTrR a.out > a.s 

e lo smontaggio non sembra spiegare il risultato prestazioni di I ottenuto:

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 

Sembra che il ramo di chiamata diretta stia decisamente facendo meno della funzione poin ter ramo. Ma perché il ramo del puntatore funziona più veloce della chiamata diretta?

E si noti che ho usato solo "tempo" per misurare il tempo. Ho usato clock_gettime per fare la misurazione e ho ottenuto risultati simili.

+0

Purtroppo per il formato: 'root @ ubuntu:/cm/gt # g ++ haha. cpp -o za.exx -O3 root @ ubuntu:/cm/gt # tempo ./za.exx reale \t 0m0.092s utente \t 0m0.084s sys \t 0m0.004s root @ ubuntu:/cm/gt # time./za.exx 1 reale \t 0m0.146s utente \t 0m0.072s sys \t 0m0.000s' –

+0

Entrambi i codici vengono eseguiti nello stesso tempo su gcc 4.7. – mfontanini

risposta

6

Non è completamente vero che il secondo ramo non può essere inarcato. Infatti, tutti i puntatori di funzione memorizzati nell'array vengono visualizzati in fase di compilazione. Quindi il compilatore può sostituire le chiamate di funzione indirette tramite chiamate dirette (e lo fa). In teoria può andare oltre e in linea (e in questo caso abbiamo due rami identici). Ma questo particolare compilatore non è abbastanza intelligente da farlo.

Come risultato, il primo ramo è ottimizzato "meglio". Ma con un'eccezione. Il compilatore non è autorizzato a ottimizzare la variabile volatile sum. Come si può vedere dal codice disassemblato, questo produce istruzioni memorizzare immediatamente seguita da istruzioni di carico (in base alle istruzioni del deposito):

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

di Intel Manuale Software Optimization (sezione 3.6.5.2) non raccomanda l'organizzazione istruzioni come questo:

... se un carico è programmato troppo presto dopo che il negozio dipende o se la generazione dei dati da memorizzare è ritardata, può esserci una penalità significativa.

Il secondo ramo evita questo problema a causa di ulteriori istruzioni di chiamata/ritorno tra archivio e carico. Quindi funziona meglio.

miglioramenti simili possono essere fatte per il primo ramo se si aggiungono alcuni (non molto costoso) calcoli in-tra:

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

Questo effetto può essere verificato anche modificando un po 'le cose nel programma di esempio in modo che ci siano 4 variabili volatili separate e 4 funzioni inline separate. La versione inline impiega metà del tempo della versione non inline. –

+1

Penso che l'opinione del compilatore sia che la variabile non sia volatile e che non memorizzerà e leggerà immediatamente, o che la variabile è volatile e l'utente lo ha richiesto, quindi non tenteranno di ottimizzare. –

+0

Il motivo per cui ho usato la variabile volatile è impedire al compilatore di ottimizzare l'intera funzione. – Ming

Problemi correlati