Ho notato per la prima volta nel 2009 che GCC (almeno sui miei progetti e sulle mie macchine) ha la tendenza a generare un codice notevolmente più veloce se ottimizzo per dimensioni ( -Os
) anziché velocità ( -O2
o -O3
), e mi chiedo da allora perché.
Sono riuscito a creare un codice (piuttosto stupido) che mostra questo comportamento sorprendente ed è sufficientemente piccolo per essere pubblicato qui.
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
Se lo compilo -Os
, ci vogliono 0,38 secondi per eseguire questo programma e 0,44 secondi se è compilato con -O2
o -O3
. Questi tempi sono ottenuti in modo coerente e praticamente senza rumore (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).
(Aggiornamento: ho spostato tutto il codice assembly in GitHub : hanno reso il post gonfio e apparentemente aggiungono molto poco valore alle domande poiché le fno-align-*
bandiere hanno lo stesso effetto.)
Ecco l'assieme generato con -Os
e -O2
.
Sfortunatamente, la mia comprensione dell'assemblaggio è molto limitata, quindi non ho idea se ciò che ho fatto dopo sia stato corretto: ho afferrato l'assemblaggio per -O2
e ho unito tutte le sue differenze nell'assemblaggio per -Os
tranne le .p2align
linee, risultato qui . Questo codice funziona ancora in 0.38s e l'unica differenza è la .p2align
roba.
Se immagino correttamente, si tratta di imbottiture per l'allineamento dello stack. Secondo Perché il pad GCC funziona con i NOP? viene fatto nella speranza che il codice venga eseguito più velocemente, ma a quanto pare questa ottimizzazione è fallita nel mio caso.
È l'imbottitura che è il colpevole in questo caso? Perché e come?
Il rumore che produce praticamente rende impossibili le microottimizzazioni temporali.
Come posso assicurarmi che tali allineamenti fortunati / sfortunati accidentali non interferiscano quando eseguo microottimizzazioni (non correlate all'allineamento dello stack) sul codice sorgente C o C ++?
AGGIORNARE:
Seguendo la risposta di Pascal Cuoq ho armeggiato un po 'con gli allineamenti. Passando -O2 -fno-align-functions -fno-align-loops
a gcc, tutti .p2align
scompaiono dall'assembly e l'eseguibile generato viene eseguito in 0.38s. Secondo la documentazione di gcc :
-Os abilita tutte le ottimizzazioni -O2 [ma] -Os disabilita i seguenti flag di ottimizzazione:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
Quindi, sembra quasi un problema di allineamento (errato).
Sono ancora scettico riguardo a -march=native
quanto suggerito nella risposta di Marat Dukhan . Non sono convinto che non stia solo interferendo con questo (mis) problema di allineamento; non ha assolutamente alcun effetto sulla mia macchina. (Tuttavia, ho votato a favore della sua risposta.)
AGGIORNAMENTO 2:
Possiamo togliere -Os
la foto. I seguenti tempi sono ottenuti compilando con
-O2 -fno-omit-frame-pointer
0.37s-O2 -fno-align-functions -fno-align-loops
0.37s-S -O2
quindi spostando manualmente il gruppoadd()
dopowork()
0,37 secondi-O2
0.44s
Mi sembra che la distanza add()
dal sito di chiamata sia molto importante. Ho provato perf
, ma l'output di perf stat
e perf report
ha poco senso per me. Tuttavia, ho potuto ottenere solo un risultato coerente da esso:
-O2
:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
Per fno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
Per -fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
Sembra che ci stiamo bloccando sulla chiamata add()
nel caso lento.
Ho esaminato tutto ciò che perf -e
può sputare sulla mia macchina; non solo le statistiche fornite sopra.
Per lo stesso eseguibile, stalled-cycles-frontend
mostra una correlazione lineare con il tempo di esecuzione; Non ho notato nient'altro che sarebbe correlato così chiaramente. (Il confronto stalled-cycles-frontend
per diversi eseguibili non ha senso per me.)
Ho incluso la cache mancante come è emerso come il primo commento. Ho esaminato tutte le mancate cache che possono essere misurate sulla mia macchina perf
, non solo quelle indicate sopra. I mancati cache sono molto rumorosi e mostrano poca o nessuna correlazione con i tempi di esecuzione.