std :: векторна регресія продуктивності при включенні C ++ 11


235

Я знайшов цікаву регресію ефективності в невеликому фрагменті C ++, коли я вмикаю 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;
}

З g ++ (GCC) 4.8.2 20131219 (передвипуск) та C ++ 03 я отримую:

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% )

З іншого боку, якщо C ++ 11 увімкнено, продуктивність значно знижується:

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% )

Хтось може це пояснити? До цього мого досвіду було те, що STL стає швидшим завдяки включенню C ++ 11, esp. завдяки семантиці руху.

EDIT: Як запропоновано, використовуючи container.emplace_back();натомість продуктивність стає нарівні з версією C ++ 03. Як версія C ++ 03 може досягти того ж 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
Якщо ви збираєтесь до складання, ви можете побачити, що відбувається під капотом. Дивіться також stackoverflow.com/questions/8021874/…
Cogwheel

8
Що станеться, якщо ви перейдете push_back(Item())на emplace_back()версію C ++ 11?
Cogwheel

8
Дивіться вище, що "фіксує" регресію. Мені все ще цікаво, чому push_back регресує у роботі між C ++ 03 та C ++ 11, хоча.
milianw

1
@milianw Виявляється, я складав неправильну програму. Ігноруйте мої коментарі.

2
З clang3.4 версія C ++ 11 швидша, 0,047s проти 0,058 для версії C ++ 98
преторіанська

Відповіді:


247

Я можу відтворити ваші результати на своїй машині за допомогою тих варіантів, які ви пишете у своєму дописі.

Однак якщо я також включу оптимізацію часу зв'язку (я також передаю -fltoпрапор до gcc 4.7.2), результати однакові:

(Я складаю ваш початковий код, з 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  

Що стосується причин, потрібно подивитися на згенерований код складання ( g++ -std=c++11 -O3 -S regr.cpp). У режимі C ++ 11 згенерований код значно більш захаращений, ніж у режимі C ++ 98, і вбудована функція
void std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
не працює в режимі C ++ 11 за замовчуванням inline-limit.

Цей невдалий інлайнер має ефект доміно. Не тому, що ця функція викликається (вона навіть не називається!), А тому, що ми повинні бути готові: якщо вона викликається, аргументи функції ( Item.aі Item.b) повинні бути вже в потрібному місці. Це призводить до досить безладного коду.

Ось відповідна частина згенерованого коду для випадку, коли вбудова вдається :

.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    #,

Це приємно і компактно для петлі. Тепер порівняємо це з тим, що не вдалося здійснити вбудований випадок:

.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_   #

Цей код захаращений, і в циклі відбувається набагато більше, ніж у попередньому випадку. Перед функцією call(відображається останній рядок) аргументи повинні бути розміщені належним чином:

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

Незважаючи на те, що це ніколи насправді не виконується, цикл упорядковує речі раніше:

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

Це призводить до безладного коду. Якщо немає функції callчерез те, що вбудовування є успішним, у нас є лише 2 вказівки переміщення в циклі, і немає змішування з %rsp(покажчик стека). Однак, якщо вкладиш не вдасться, ми отримуємо 6 ходів і ми багато возимося з %rsp.

Просто для обґрунтування моєї теорії (зверніть увагу -finline-limit), обидва в режимі 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% )

Дійсно, якщо ми попросимо компілятора спробувати трохи складніше вбудувати цю функцію, різниця в продуктивності знижується.


То що ж таке відбирає від цієї історії? Цей невдалий рядок може коштувати вам дорого, і ви повинні повністю використовувати можливості компілятора: я можу лише рекомендувати оптимізацію часу зв'язку. Це дало значне підвищення продуктивності моїм програмам (до 2,5х), і все, що мені потрібно було зробити, - це передавати -fltoпрапор. Це досить гарна угода! ;)

Однак я не рекомендую перебирати ваш код ключовим словом inline; нехай компілятор вирішить, що робити. (Оптимізатору дозволено трактувати ключове слово вбудований як пробіл.)


Чудове запитання, +1!


3
NB: не inlineмає нічого спільного з функцією вбудовування; це означає "визначений рядок", а не "будь ласка, вкажіть це". Якщо ви хочете насправді просити вкладиші, скористайтеся __attribute__((always_inline))чи подібним.
Джон Перді,

2
@JonPurdy Не зовсім, наприклад, функції члена класу неявно вбудовані. inlineце також запит до компілятора про те, що ви хочете, щоб ця функція була вбудована, і, наприклад, компілятор Intel C ++ використовувався для попередження про продуктивність, якщо він не виконав ваш запит. (Я нещодавно не перевіряв icc, якщо він все-таки є.) На жаль, я бачив, як люди збивають свій код inlineі чекають, коли станеться чудо. Я б не користувався __attribute__((always_inline)); Швидше за все, розробники компілятора краще знають, що потрібно робити, а що не робити. (Незважаючи на контрприклад тут.)
Алі

1
@JonPurdy З іншого боку, якщо ви визначите функцію вбудованої функції, яка не є функцією-членом класу , то у вас дійсно немає іншого вибору, крім того, щоб позначити її в рядку, інакше ви отримаєте кілька помилок визначення від лінкера. Якщо це ви мали на увазі, тоді гаразд.
Алі

1
Так, це я мав на увазі. Стандарт говорить: "Специфікатор inlineвказує на реалізацію, що вбудована підміна функції функції в точці виклику повинна віддавати перевагу звичайному механізму виклику функції". (§7.1.2.2) Однак для здійснення цієї оптимізації не потрібно впроваджувати, оскільки це значною мірою збіг обставин, коли inlineфункції часто бувають хорошими кандидатами для вбудовування. Тож краще бути явним і використовувати прагму компілятора.
Джон Перді,

3
@JonPurdy Що стосується першої половини: Так, саме це я мав на увазі, кажучи: " Оптимізатору дозволено трактувати ключове слово вбудованого простору як пробіл". Що стосується прагми компілятора, я б не використовував це, я б залишав це до оптимізації часу зв’язку, вбудовувати чи ні. Це робить досить гарну роботу; він також автоматично вирішив це питання, обговорене тут у відповіді.
Алі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.