Perché GCC genera un codice più veloce del 15-20% se ottimizzo per dimensioni anziché velocità?


445

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à ( -O2o -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 -O2o -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 -Ose -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 -O2e ho unito tutte le sue differenze nell'assemblaggio per -Os tranne le .p2alignlinee, 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-loopsa gcc, tutti .p2alignscompaiono 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=nativequanto 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 -Osla 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 -O2quindi spostando manualmente il gruppo add()dopo work()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 state perf reportha 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 -epuò sputare sulla mia macchina; non solo le statistiche fornite sopra.

Per lo stesso eseguibile, stalled-cycles-frontendmostra una correlazione lineare con il tempo di esecuzione; Non ho notato nient'altro che sarebbe correlato così chiaramente. (Il confronto stalled-cycles-frontendper 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.


36
Supposizione cieca: può essere un errore nella cache?

@ H2CO3 Era anche il mio primo pensiero, ma non era abbastanza incoraggiato a pubblicare il commento senza leggere e comprendere a fondo la domanda del PO.
πάντα ῥεῖ

2
@ g-makulik Ecco perché ho avvertito che si tratta di una "supposizione cieca" ;-) "TL; DR" è riservato per domande sbagliate. : P

3
Solo un punto dati interessante: trovo che -O3 o -Ofast sia circa 1,5x più veloce di -Os quando lo compilo con clang su OS X. (Non ho provato a riprodurre con gcc.)
Rob Napier,

2
È lo stesso codice. Dai un'occhiata più da vicino all'indirizzo .L3, gli obiettivi di filiale disallineati sono costosi.
Hans Passant,

Risposte:


506

Per impostazione predefinita, i compilatori ottimizzano per il processore "medio". Poiché processori diversi favoriscono sequenze di istruzioni diverse, le ottimizzazioni del compilatore abilitate da -O2potrebbero favorire il processore medio, ma ridurre le prestazioni sul tuo particolare processore (e lo stesso vale per -Os). Se provi lo stesso esempio su processori diversi, scoprirai che su alcuni di essi trarranno vantaggio -O2mentre altri sono più favorevoli alle -Osottimizzazioni.

Ecco i risultati per time ./test 0 0diversi processori (tempo utente riportato):

Processor (System-on-Chip)             Compiler   Time (-O2)  Time (-Os)  Fastest
AMD Opteron 8350                       gcc-4.8.1    0.704s      0.896s      -O2
AMD FX-6300                            gcc-4.8.1    0.392s      0.340s      -Os
AMD E2-1800                            gcc-4.7.2    0.740s      0.832s      -O2
Intel Xeon E5405                       gcc-4.8.1    0.603s      0.804s      -O2
Intel Xeon E5-2603                     gcc-4.4.7    1.121s      1.122s       -
Intel Core i3-3217U                    gcc-4.6.4    0.709s      0.709s       -
Intel Core i3-3217U                    gcc-4.7.3    0.708s      0.822s      -O2
Intel Core i3-3217U                    gcc-4.8.1    0.708s      0.944s      -O2
Intel Core i7-4770K                    gcc-4.8.1    0.296s      0.288s      -Os
Intel Atom 330                         gcc-4.8.1    2.003s      2.007s      -O2
ARM 1176JZF-S (Broadcom BCM2835)       gcc-4.6.3    3.470s      3.480s      -O2
ARM Cortex-A8 (TI OMAP DM3730)         gcc-4.6.3    2.727s      2.727s       -
ARM Cortex-A9 (TI OMAP 4460)           gcc-4.6.3    1.648s      1.648s       -
ARM Cortex-A9 (Samsung Exynos 4412)    gcc-4.6.3    1.250s      1.250s       -
ARM Cortex-A15 (Samsung Exynos 5250)   gcc-4.7.2    0.700s      0.700s       -
Qualcomm Snapdragon APQ8060A           gcc-4.8       1.53s       1.52s      -Os

In alcuni casi puoi alleviare l'effetto di ottimizzazioni svantaggiose chiedendo gccdi ottimizzare per il tuo particolare processore (utilizzando le opzioni -mtune=nativeo -march=native):

Processor            Compiler   Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300          gcc-4.8.1         0.340s                   0.340s
AMD E2-1800          gcc-4.7.2         0.740s                   0.832s
Intel Xeon E5405     gcc-4.8.1         0.603s                   0.803s
Intel Core i7-4770K  gcc-4.8.1         0.296s                   0.288s

Aggiornamento: su Core i3 basato su Ivy Bridge tre versioni di gcc( 4.6.4, 4.7.3e 4.8.1) producono file binari con prestazioni significativamente diverse, ma il codice assembly presenta solo sottili variazioni. Finora non ho alcuna spiegazione di questo fatto.

Assemblaggio da gcc-4.6.4 -Os(eseguito in 0,709 secondi):

00000000004004d2 <_ZL3addRKiS0_.isra.0>:
  4004d2:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004d5:       c3                      ret

00000000004004d6 <_ZL4workii>:
  4004d6:       41 55                   push   r13
  4004d8:       41 89 fd                mov    r13d,edi
  4004db:       41 54                   push   r12
  4004dd:       41 89 f4                mov    r12d,esi
  4004e0:       55                      push   rbp
  4004e1:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  4004e6:       53                      push   rbx
  4004e7:       31 db                   xor    ebx,ebx
  4004e9:       41 8d 34 1c             lea    esi,[r12+rbx*1]
  4004ed:       41 8d 7c 1d 00          lea    edi,[r13+rbx*1+0x0]
  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>
  4004fd:       89 d8                   mov    eax,ebx
  4004ff:       5b                      pop    rbx
  400500:       5d                      pop    rbp
  400501:       41 5c                   pop    r12
  400503:       41 5d                   pop    r13
  400505:       c3                      ret

Assemblaggio da gcc-4.7.3 -Os(eseguito in 0,822 secondi):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

00000000004004fe <_ZL4workii>:
  4004fe:       41 55                   push   r13
  400500:       41 89 f5                mov    r13d,esi
  400503:       41 54                   push   r12
  400505:       41 89 fc                mov    r12d,edi
  400508:       55                      push   rbp
  400509:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  40050e:       53                      push   rbx
  40050f:       31 db                   xor    ebx,ebx
  400511:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400516:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>
  40051f:       01 c3                   add    ebx,eax
  400521:       ff cd                   dec    ebp
  400523:       75 ec                   jne    400511 <_ZL4workii+0x13>
  400525:       89 d8                   mov    eax,ebx
  400527:       5b                      pop    rbx
  400528:       5d                      pop    rbp
  400529:       41 5c                   pop    r12
  40052b:       41 5d                   pop    r13
  40052d:       c3                      ret

Assemblaggio da gcc-4.8.1 -Os(eseguito in 0,994 secondi):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3                      ret

0000000000400501 <_ZL4workii>:
  400501:       41 55                   push   r13
  400503:       41 89 f5                mov    r13d,esi
  400506:       41 54                   push   r12
  400508:       41 89 fc                mov    r12d,edi
  40050b:       55                      push   rbp
  40050c:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  400511:       53                      push   rbx
  400512:       31 db                   xor    ebx,ebx
  400514:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400519:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051d:       e8 db ff ff ff          call   4004fd <_ZL3addRKiS0_.isra.0>
  400522:       01 c3                   add    ebx,eax
  400524:       ff cd                   dec    ebp
  400526:       75 ec                   jne    400514 <_ZL4workii+0x13>
  400528:       89 d8                   mov    eax,ebx
  40052a:       5b                      pop    rbx
  40052b:       5d                      pop    rbp
  40052c:       41 5c                   pop    r12
  40052e:       41 5d                   pop    r13
  400530:       c3                      ret

187
Giusto per chiarire: sei andato davvero a misurare le prestazioni del codice OP su 12 piattaforme diverse? (+1 per il solo pensiero che lo faresti)
anatolyg

194
@anatolyg Sì, l'ho fatto! (e ne aggiungerò presto altri)
Marat Dukhan,

43
Infatti. Un altro +1 per non solo teorizzare su CPU diverse ma dimostrarlo effettivamente . Non qualcosa (ahimè) che vedi in ogni risposta sulla velocità. Questi test vengono eseguiti con lo stesso sistema operativo? (Come potrebbe essere possibile, ciò distorce il risultato ...)
usr2564301

7
@Ali On AMD-FX 6300 -O2 -fno-align-functions -fno-align-loopsriduce il tempo a 0.340s, quindi potrebbe essere spiegato dall'allineamento. Tuttavia, l'allineamento ottimale dipende dal processore: alcuni processori preferiscono loop e funzioni allineati.
Marat Dukhan,

13
@Jongware Non vedo come il sistema operativo influenzerebbe significativamente i risultati; il loop non effettua mai chiamate di sistema.
Ali,

186

Il mio collega mi ha aiutato a trovare una risposta plausibile alla mia domanda. Ha notato l'importanza del limite di 256 byte. Non è registrato qui e mi ha incoraggiato a pubblicare la risposta da solo (e prendere tutta la fama).


Risposta breve:

È l'imbottitura che è il colpevole in questo caso? Perché e come?

Tutto si riduce all'allineamento. Gli allineamenti possono avere un impatto significativo sulle prestazioni, ecco perché abbiamo le -falign-*bandiere in primo luogo.

Ho inviato una segnalazione di bug (fasullo?) Agli sviluppatori di gcc . Si scopre che il comportamento predefinito è "allineamo i loop a 8 byte per impostazione predefinita, ma proviamo ad allinearlo a 16 byte se non è necessario compilare più di 10 byte". Apparentemente, questo valore predefinito non è la scelta migliore in questo caso particolare e sulla mia macchina. Clang 3.4 (trunk) con -O3esegue l'allineamento appropriato e il codice generato non mostra questo strano comportamento.

Naturalmente, se viene fatto un allineamento inappropriato, le cose peggiorano. Un allineamento non necessario / errato consuma byte senza motivo e potenzialmente aumenta i mancati cache, ecc.

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) sui codici sorgente C o C ++?

Semplicemente dicendo a gcc di fare il giusto allineamento:

g++ -O2 -falign-functions=16 -falign-loops=16


Risposta lunga:

Il codice verrà eseguito più lentamente se:

  • un XXconfine di byte taglia add()nel mezzo ( XXdipende dalla macchina).

  • se la chiamata a add()deve superare un XXlimite di byte e la destinazione non è allineata.

  • se add()non è allineato.

  • se il loop non è allineato.

I primi 2 sono ben visibili sui codici e sui risultati che Marat Dukhan ha gentilmente pubblicato . In questo caso, gcc-4.8.1 -Os(viene eseguito in 0,994 secondi):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3   

un limite di 256 byte taglia add()nel mezzo e add()né il ciclo né è allineato. Sorpresa, sorpresa, questo è il caso più lento!

Nel caso gcc-4.7.3 -Os(viene eseguito in 0,822 secondi), il limite di 256 byte taglia solo in una sezione fredda (ma né il ciclo, né add()viene tagliato):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

[...]

  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>

Nulla è allineato e la chiamata a add()deve superare il limite di 256 byte. Questo codice è il secondo più lento.

Nel caso gcc-4.6.4 -Os(viene eseguito in 0,709 secondi), sebbene nulla sia allineato, la chiamata a add()non deve saltare oltre il limite di 256 byte e la destinazione è esattamente a 32 byte di distanza:

  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>

Questo è il più veloce di tutti e tre. Perché il limite di 256 byte è parlato sulla sua macchina, lo lascerò a lui per capirlo. Non ho un tale processore.

Ora, sulla mia macchina non ottengo questo effetto limite di 256 byte. Solo la funzione e l'allineamento del loop entrano in funzione sulla mia macchina. Se passo, g++ -O2 -falign-functions=16 -falign-loops=16tutto torna alla normalità: ottengo sempre il caso più veloce e il tempo non è più sensibile alla -fno-omit-frame-pointerbandiera. Posso passare g++ -O2 -falign-functions=32 -falign-loops=32o eventuali multipli di 16, il codice non è sensibile neanche a quello.

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) invece di velocità (-O2 o -O3) e mi chiedevo da allora perché.

Una probabile spiegazione è che avevo degli hotspot sensibili all'allineamento, proprio come quello in questo esempio. Facendo casino con le bandiere (passando -Osinvece che -O2), quegli hotspot sono stati allineati in modo fortunato per caso e il codice è diventato più veloce. Non aveva nulla a che fare con l'ottimizzazione delle dimensioni: questi erano per puro caso che gli hotspot si allineavano meglio. D'ora in poi, controllerò gli effetti dell'allineamento sui miei progetti.

Oh, e un'altra cosa. Come possono sorgere tali hotspot, come quello mostrato nell'esempio? Come può add()fallire l'inserimento di una funzione così piccola come quella ?

Considera questo:

// add.cpp
int add(const int& x, const int& y) {
    return x + y;
}

e in un file separato:

// main.cpp
int add(const int& x, const int& y);

const int LOOP_BOUND = 200000000;

__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;
}

e compilato come: g++ -O2 add.cpp main.cpp.

      gcc non sarà in linea add()!

Questo è tutto, è così facile creare involontariamente hotspot come quello nell'OP. Naturalmente è in parte colpa mia: gcc è un compilatore eccellente. Se compili quanto sopra come: g++ -O2 -flto add.cpp main.cppcioè se eseguo l'ottimizzazione del tempo di collegamento, il codice viene eseguito in 0.19s!

(L'allineamento è disabilitato artificialmente nell'OP, quindi il codice nell'OP era 2 volte più lento).


19
Wow ... Questo va decisamente oltre ciò che faccio di solito per aggirare le anomalie di benchmarking.
Mistico il

@Ali immagino abbia senso dal momento che come può il compilatore incorporare qualcosa che non vede? Questo è probabilmente il motivo per cui usiamo la inlinedefinizione della funzione + nell'intestazione. Non sono sicuro di quanto sia maturo lto in gcc. La mia esperienza con esso almeno in Mingw è un successo.
greatwolf,

7
Penso che sia stato Communications dell'ACM che ha pubblicato un articolo qualche anno fa sull'esecuzione di applicazioni abbastanza grandi (perl, Spice, ecc.) Mentre spostava l'intera immagine binaria di un byte alla volta utilizzando ambienti Linux di dimensioni diverse. Ricordo una varianza tipica del 15% circa. Il loro riassunto era che molti risultati di benchmark sono inutili perché questa variabile esterna di allineamento non viene presa in considerazione.
Gene,

1
in particolare per -flto. è abbastanza rivoluzionario se non l'hai mai usato prima, parlando per esperienza :)
underscore_d

2
Questo è un video fantastico che parla di come l'allineamento può influire sulle prestazioni e come
creare un

73

Sto aggiungendo questo post-accettazione per sottolineare che gli effetti dell'allineamento sulle prestazioni complessive dei programmi, compresi quelli di grandi dimensioni, sono stati studiati. Ad esempio, questo articolo (e credo che una versione di questo sia apparsa anche in CACM) mostra come le modifiche all'ordine dei collegamenti e alle dimensioni dell'ambiente del sistema operativo da sole siano state sufficienti per spostare significativamente le prestazioni. Lo attribuiscono all'allineamento di "hot loop".

Questo documento, intitolato "Produrre dati errati senza fare ovviamente nulla di sbagliato!" afferma che la distorsione sperimentale involontaria a causa di differenze quasi incontrollabili negli ambienti di esecuzione del programma probabilmente rende insignificanti molti risultati di benchmark.

Penso che stai incontrando un angolo diverso sulla stessa osservazione.

Per il codice critico per le prestazioni, questo è un argomento piuttosto valido per i sistemi che valutano l'ambiente in fase di installazione o di esecuzione e scelgono il migliore locale tra le versioni ottimizzate in modo diverso delle routine chiave.


33

Penso che puoi ottenere lo stesso risultato di quello che hai fatto:

Ho preso l'assembly per -O2 e ho unito tutte le sue differenze nell'assemblaggio per -Os tranne le linee .p2align:

... usando -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Ho compilato tutto con queste opzioni, che erano più veloci del normale -O2ogni volta che mi sono preso la briga di misurare, per 15 anni.

Inoltre, per un contesto completamente diverso (incluso un altro compilatore), ho notato che la situazione è simile : l'opzione che dovrebbe "ottimizzare le dimensioni del codice anziché la velocità" ottimizza per dimensioni e velocità del codice.

Se immagino correttamente, si tratta di imbottiture per l'allineamento dello stack.

No, questo non ha nulla a che fare con lo stack, i NOP generati per impostazione predefinita e che le opzioni -falign - * = 1 impediscono l'allineamento del codice.

Secondo Perché il pad GCC funziona con i NOP? viene fatto nella speranza che il codice venga eseguito più velocemente, ma apparentemente questa ottimizzazione è fallita nel mio caso.

È l'imbottitura che è il colpevole in questo caso? Perché e come?

È molto probabile che l'imbottitura sia il colpevole. Il motivo per cui il padding è ritenuto necessario ed è utile in alcuni casi è che il codice viene in genere recuperato in righe di 16 byte (vedere le risorse di ottimizzazione della nebbia di Agner per i dettagli, che variano in base al modello di processore). Allineare una funzione, un ciclo o un'etichetta su un limite di 16 byte significa che le probabilità sono aumentate statisticamente che sarà necessario un numero inferiore di righe per contenere la funzione o il ciclo. Ovviamente, si ritorcerà contro perché questi NOP riducono la densità del codice e quindi l'efficienza della cache. Nel caso di loop ed etichette, potrebbe anche essere necessario eseguire i NOP una volta (quando l'esecuzione arriva normalmente al loop / etichetta, al contrario di un salto).


La cosa divertente è: -O2 -fno-omit-frame-pointerè buono come -Os. Si prega di controllare la domanda aggiornata.
Ali,

11

Se il tuo programma è limitato dalla cache CODE L1, allora l'ottimizzazione per le dimensioni inizia improvvisamente a pagare.

L'ultima volta che ho verificato, il compilatore non è abbastanza intelligente da capirlo in tutti i casi.

Nel tuo caso, -O3 probabilmente genera codice sufficiente per due righe della cache, ma -O si adatta a una riga della cache.


1
Quanto vuoi scommettere che i parametri align = si riferiscono alla dimensione delle linee della cache?
Giosuè,

Non mi interessa più davvero: non è visibile sulla mia macchina. E passando le -falign-*=16bandiere, tutto è tornato alla normalità, tutto si comporta in modo coerente. Per quanto mi riguarda, questa domanda è stata risolta.
Ali,

7

Non sono affatto un esperto in questo settore, ma mi sembra di ricordare che i processori moderni sono piuttosto sensibili quando si tratta di previsione dei rami . Gli algoritmi utilizzati per predire i rami sono (o almeno risalivano ai giorni in cui ho scritto il codice assembler) in base a diverse proprietà del codice, tra cui la distanza di un bersaglio e la direzione.

Lo scenario che viene in mente sono piccoli anelli. Quando il ramo stava andando indietro e la distanza non era troppo lontana, la previsione del ramo si stava ottimizzando per questo caso poiché tutti i piccoli anelli sono fatti in questo modo. Le stesse regole potrebbero entrare in gioco quando si scambia la posizione di adde worknel codice generato o quando la posizione di entrambi cambia leggermente.

Detto questo, non ho idea di come verificarlo e volevo solo farti sapere che questo potrebbe essere qualcosa che vuoi esaminare.


Grazie. Ci ho giocato: posso solo accelerare scambiando add()e work()se -O2viene superato. In tutti gli altri casi il codice diventa notevolmente più lento scambiando. Durante il fine settimana, ho anche analizzato le statistiche di previsione / errata previsione delle filiali perfe non ho notato nulla che potesse spiegare questo strano comportamento. L'unico risultato coerente è che nel caso lento perfriporta 100,0 pollici add()e un valore elevato sulla linea subito dopo la chiamata a add()nel ciclo. Sembra che ci stiamo bloccando per qualche motivo add()nel caso lento ma non nelle corse veloci.
Ali,

Sto pensando di installare Intel VTune su una delle mie macchine e fare un profilo da solo. perfsupporta solo un numero limitato di cose, forse le cose di Intel sono un po 'più utili sul proprio processore.
Ali,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.