regressione delle prestazioni std :: vector quando si abilita C ++ 11


235

Ho trovato un'interessante regressione delle prestazioni in un piccolo frammento di C ++, quando abilito C ++ 11:

#include <vector>

struct Item
{
  int a;
  int b;
};

int main()
{
  const std::size_t num_items = 10000000;
  std::vector<Item> container;
  container.reserve(num_items);
  for (std::size_t i = 0; i < num_items; ++i) {
    container.push_back(Item());
  }
  return 0;
}

Con g ++ (GCC) 4.8.2 20131219 (pre-release) e C ++ 03 ottengo:

milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        35.206824 task-clock                #    0.988 CPUs utilized            ( +-  1.23% )
                4 context-switches          #    0.116 K/sec                    ( +-  4.38% )
                0 cpu-migrations            #    0.006 K/sec                    ( +- 66.67% )
              849 page-faults               #    0.024 M/sec                    ( +-  6.02% )
       95,693,808 cycles                    #    2.718 GHz                      ( +-  1.14% ) [49.72%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
       95,282,359 instructions              #    1.00  insns per cycle          ( +-  0.65% ) [75.27%]
       30,104,021 branches                  #  855.062 M/sec                    ( +-  0.87% ) [77.46%]
            6,038 branch-misses             #    0.02% of all branches          ( +- 25.73% ) [75.53%]

      0.035648729 seconds time elapsed                                          ( +-  1.22% )

Con C ++ 11 abilitato, invece, le prestazioni si riducono in modo significativo:

milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        86.485313 task-clock                #    0.994 CPUs utilized            ( +-  0.50% )
                9 context-switches          #    0.104 K/sec                    ( +-  1.66% )
                2 cpu-migrations            #    0.017 K/sec                    ( +- 26.76% )
              798 page-faults               #    0.009 M/sec                    ( +-  8.54% )
      237,982,690 cycles                    #    2.752 GHz                      ( +-  0.41% ) [51.32%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
      135,730,319 instructions              #    0.57  insns per cycle          ( +-  0.32% ) [75.77%]
       30,880,156 branches                  #  357.057 M/sec                    ( +-  0.25% ) [75.76%]
            4,188 branch-misses             #    0.01% of all branches          ( +-  7.59% ) [74.08%]

    0.087016724 seconds time elapsed                                          ( +-  0.50% )

Qualcuno può spiegare questo? Finora la mia esperienza è stata che l'STL diventa più veloce abilitando C ++ 11, esp. grazie per spostare la semantica.

EDIT: Come suggerito, usando container.emplace_back();invece le prestazioni va alla pari con la versione C ++ 03. Come può la versione C ++ 03 ottenere lo stesso per push_back?

milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        36.229348 task-clock                #    0.988 CPUs utilized            ( +-  0.81% )
                4 context-switches          #    0.116 K/sec                    ( +-  3.17% )
                1 cpu-migrations            #    0.017 K/sec                    ( +- 36.85% )
              798 page-faults               #    0.022 M/sec                    ( +-  8.54% )
       94,488,818 cycles                    #    2.608 GHz                      ( +-  1.11% ) [50.44%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
       94,851,411 instructions              #    1.00  insns per cycle          ( +-  0.98% ) [75.22%]
       30,468,562 branches                  #  840.991 M/sec                    ( +-  1.07% ) [76.71%]
            2,723 branch-misses             #    0.01% of all branches          ( +-  9.84% ) [74.81%]

   0.036678068 seconds time elapsed                                          ( +-  0.80% )

1
Se esegui la compilazione per l'assemblaggio, puoi vedere cosa succede sotto il cofano. Vedi anche stackoverflow.com/questions/8021874/…
Ruota dentata

8
Cosa succede se si push_back(Item())passa a emplace_back()nella versione C ++ 11?
Ruota dentata

8
Vedi sopra, che "corregge" la regressione. Mi chiedo ancora perché push_back regredisca nelle prestazioni tra C ++ 03 e C ++ 11.
milianw,

1
@milianw Si scopre che stavo compilando il programma sbagliato. Ignora i miei commenti.

2
Con clang3.4 la versione C ++ 11 è più veloce, 0,047s contro 0,058 per la versione C ++ 98
Pretorio

Risposte:


247

Posso riprodurre i tuoi risultati sulla mia macchina con quelle opzioni che scrivi nel tuo post.

Tuttavia, se abilito anche l' ottimizzazione del tempo di collegamento (passo anche il -fltoflag a gcc 4.7.2), i risultati sono identici:

(Sto compilando il tuo codice originale, con container.push_back(Item());)

$ g++ -std=c++11 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.426793 task-clock                #    0.986 CPUs utilized            ( +-  1.75% )
                 4 context-switches          #    0.116 K/sec                    ( +-  5.69% )
                 0 CPU-migrations            #    0.006 K/sec                    ( +- 66.67% )
            19,801 page-faults               #    0.559 M/sec                  
        99,028,466 cycles                    #    2.795 GHz                      ( +-  1.89% ) [77.53%]
        50,721,061 stalled-cycles-frontend   #   51.22% frontend cycles idle     ( +-  3.74% ) [79.47%]
        25,585,331 stalled-cycles-backend    #   25.84% backend  cycles idle     ( +-  4.90% ) [73.07%]
       141,947,224 instructions              #    1.43  insns per cycle        
                                             #    0.36  stalled cycles per insn  ( +-  0.52% ) [88.72%]
        37,697,368 branches                  # 1064.092 M/sec                    ( +-  0.52% ) [88.75%]
            26,700 branch-misses             #    0.07% of all branches          ( +-  3.91% ) [83.64%]

       0.035943226 seconds time elapsed                                          ( +-  1.79% )



$ g++ -std=c++98 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.510495 task-clock                #    0.988 CPUs utilized            ( +-  2.54% )
                 4 context-switches          #    0.101 K/sec                    ( +-  7.41% )
                 0 CPU-migrations            #    0.003 K/sec                    ( +-100.00% )
            19,801 page-faults               #    0.558 M/sec                    ( +-  0.00% )
        98,463,570 cycles                    #    2.773 GHz                      ( +-  1.09% ) [77.71%]
        50,079,978 stalled-cycles-frontend   #   50.86% frontend cycles idle     ( +-  2.20% ) [79.41%]
        26,270,699 stalled-cycles-backend    #   26.68% backend  cycles idle     ( +-  8.91% ) [74.43%]
       141,427,211 instructions              #    1.44  insns per cycle        
                                             #    0.35  stalled cycles per insn  ( +-  0.23% ) [87.66%]
        37,366,375 branches                  # 1052.263 M/sec                    ( +-  0.48% ) [88.61%]
            26,621 branch-misses             #    0.07% of all branches          ( +-  5.28% ) [83.26%]

       0.035953916 seconds time elapsed  

Per quanto riguarda i motivi, è necessario esaminare il codice assembly generato ( g++ -std=c++11 -O3 -S regr.cpp). Nella modalità C ++ 11 il codice generato è significativamente più disordinato rispetto alla modalità C ++ 98 e l' integrazione della funzione
void std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
non riesce nella modalità C ++ 11 con l'impostazione predefinita inline-limit.

Questo errore in linea ha un effetto domino. Non perché questa funzione venga chiamata (non viene nemmeno chiamata!) Ma perché dobbiamo essere preparati: se viene chiamata, la funzione argomenta ( Item.ae Item.b) deve già essere nel posto giusto. Questo porta a un codice piuttosto disordinato.

Ecco la parte rilevante del codice generato per il caso in cui l'operazione di allineamento abbia esito positivo :

.L42:
    testq   %rbx, %rbx  # container$D13376$_M_impl$_M_finish
    je  .L3 #,
    movl    $0, (%rbx)  #, container$D13376$_M_impl$_M_finish_136->a
    movl    $0, 4(%rbx) #, container$D13376$_M_impl$_M_finish_136->b
.L3:
    addq    $8, %rbx    #, container$D13376$_M_impl$_M_finish
    subq    $1, %rbp    #, ivtmp.106
    je  .L41    #,
.L14:
    cmpq    %rbx, %rdx  # container$D13376$_M_impl$_M_finish, container$D13376$_M_impl$_M_end_of_storage
    jne .L42    #,

Questo è un loop piacevole e compatto. Ora, confrontiamo questo con quello del caso inline fallito :

.L49:
    testq   %rax, %rax  # D.15772
    je  .L26    #,
    movq    16(%rsp), %rdx  # D.13379, D.13379
    movq    %rdx, (%rax)    # D.13379, *D.15772_60
.L26:
    addq    $8, %rax    #, tmp75
    subq    $1, %rbx    #, ivtmp.117
    movq    %rax, 40(%rsp)  # tmp75, container.D.13376._M_impl._M_finish
    je  .L48    #,
.L28:
    movq    40(%rsp), %rax  # container.D.13376._M_impl._M_finish, D.15772
    cmpq    48(%rsp), %rax  # container.D.13376._M_impl._M_end_of_storage, D.15772
    movl    $0, 16(%rsp)    #, D.13379.a
    movl    $0, 20(%rsp)    #, D.13379.b
    jne .L49    #,
    leaq    16(%rsp), %rsi  #,
    leaq    32(%rsp), %rdi  #,
    call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

Questo codice è disordinato e c'è molto di più nel loop rispetto al caso precedente. Prima della funzione call(ultima riga visualizzata), gli argomenti devono essere posizionati in modo appropriato:

leaq    16(%rsp), %rsi  #,
leaq    32(%rsp), %rdi  #,
call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

Anche se questo non viene mai effettivamente eseguito, il loop organizza le cose prima:

movl    $0, 16(%rsp)    #, D.13379.a
movl    $0, 20(%rsp)    #, D.13379.b

Questo porta al codice disordinato. Se non c'è alcuna funzione callperché l'inline ha esito positivo, abbiamo solo 2 istruzioni di spostamento nel loop e non c'è nessun messing con il %rsp(puntatore dello stack). Tuttavia, se l'allineamento fallisce, otteniamo 6 mosse e pasticciamo molto con %rsp.

Giusto per corroborare la mia teoria (notare la -finline-limit), entrambe in modalità C ++ 11:

 $ g++ -std=c++11 -O3 -finline-limit=105 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         84.739057 task-clock                #    0.993 CPUs utilized            ( +-  1.34% )
                 8 context-switches          #    0.096 K/sec                    ( +-  2.22% )
                 1 CPU-migrations            #    0.009 K/sec                    ( +- 64.01% )
            19,801 page-faults               #    0.234 M/sec                  
       266,809,312 cycles                    #    3.149 GHz                      ( +-  0.58% ) [81.20%]
       206,804,948 stalled-cycles-frontend   #   77.51% frontend cycles idle     ( +-  0.91% ) [81.25%]
       129,078,683 stalled-cycles-backend    #   48.38% backend  cycles idle     ( +-  1.37% ) [69.49%]
       183,130,306 instructions              #    0.69  insns per cycle        
                                             #    1.13  stalled cycles per insn  ( +-  0.85% ) [85.35%]
        38,759,720 branches                  #  457.401 M/sec                    ( +-  0.29% ) [85.43%]
            24,527 branch-misses             #    0.06% of all branches          ( +-  2.66% ) [83.52%]

       0.085359326 seconds time elapsed                                          ( +-  1.31% )

 $ g++ -std=c++11 -O3 -finline-limit=106 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         37.790325 task-clock                #    0.990 CPUs utilized            ( +-  2.06% )
                 4 context-switches          #    0.098 K/sec                    ( +-  5.77% )
                 0 CPU-migrations            #    0.011 K/sec                    ( +- 55.28% )
            19,801 page-faults               #    0.524 M/sec                  
       104,699,973 cycles                    #    2.771 GHz                      ( +-  2.04% ) [78.91%]
        58,023,151 stalled-cycles-frontend   #   55.42% frontend cycles idle     ( +-  4.03% ) [78.88%]
        30,572,036 stalled-cycles-backend    #   29.20% backend  cycles idle     ( +-  5.31% ) [71.40%]
       140,669,773 instructions              #    1.34  insns per cycle        
                                             #    0.41  stalled cycles per insn  ( +-  1.40% ) [88.14%]
        38,117,067 branches                  # 1008.646 M/sec                    ( +-  0.65% ) [89.38%]
            27,519 branch-misses             #    0.07% of all branches          ( +-  4.01% ) [86.16%]

       0.038187580 seconds time elapsed                                          ( +-  2.05% )

In effetti, se chiediamo al compilatore di provare un po 'più a fondo per incorporare quella funzione, la differenza nelle prestazioni scompare.


Allora, qual è il take away di questa storia? Questo inline fallito può costarti molto e dovresti sfruttare appieno le funzionalità del compilatore: posso solo consigliare l'ottimizzazione del tempo di collegamento. Ha dato un notevole aumento delle prestazioni ai miei programmi (fino a 2,5 volte) e tutto quello che dovevo fare è passare la -fltobandiera. È un buon affare! ;)

Tuttavia, non consiglio di eliminare il codice con la parola chiave inline; lascia che il compilatore decida cosa fare. (L'ottimizzatore può comunque trattare la parola chiave inline come uno spazio bianco.)


Ottima domanda, +1!


3
NB: inlinenon ha nulla a che fare con la funzione inline; significa "definito in linea" non "per favore, incorpora questo". Se si desidera effettivamente chiedere in linea, utilizzare __attribute__((always_inline))o simili.
Jon Purdy,

2
@JonPurdy Non del tutto, ad esempio le funzioni dei membri della classe sono implicitamente in linea. inlineè anche una richiesta al compilatore che desideri che la funzione sia incorporata e, ad esempio, il compilatore Intel C ++ utilizzato per fornire avvisi sulle prestazioni se non soddisfaceva la tua richiesta. (Non ho controllato icc di recente se lo fa ancora.) Sfortunatamente, ho visto persone che trasgredivano il loro codice inlinee aspettavano che accadesse il miracolo. Non vorrei usare __attribute__((always_inline)); è probabile che gli sviluppatori del compilatore sappiano meglio cosa incorporare e cosa no. (Nonostante il controesempio qui.)
Ali

1
@JonPurdy D'altra parte, se si definisce una funzione inline che non è una funzione membro di una classe , allora non si ha altra scelta che contrassegnarla in linea, altrimenti si otterranno più errori di definizione dal linker. Se questo è ciò che intendevi, allora OK.
Ali,

1
Sì, questo è ciò che intendevo. Lo standard dice "Lo inlinespecificatore indica all'implementazione che la sostituzione in linea del corpo della funzione nel punto di chiamata deve essere preferita al solito meccanismo di chiamata di funzione." (§7.1.2.2) Tuttavia, le implementazioni non sono necessarie per eseguire tale ottimizzazione, poiché è in gran parte una coincidenza che inlinespesso le funzioni siano buoni candidati per l'inserimento. Quindi è meglio essere espliciti e usare un pragma di compilatore.
Jon Purdy,

3
@JonPurdy Per quanto riguarda la prima metà: Sì, questo è ciò che intendevo dicendo "L' ottimizzatore è autorizzato a trattare comunque la parola chiave inline come spazio bianco". Per quanto riguarda il pragma del compilatore, non lo userei, lo lascerei all'ottimizzazione del tempo di collegamento sia in linea che in linea. Fa un ottimo lavoro; ha anche risolto automaticamente questo problema discusso qui nella risposta.
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.