2014-01-07 3 views
228

Я нашел интересный регресс производительности в небольших C++ сниппет, когда я включаю C++ 11:станда :: вектор производительность регрессии при включении 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; 
} 

С г ++ (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%) 

с 11 C++ включен с другой стороны, производительность падает значительно:

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, особенно. благодаря семантике перемещения.

EDIT: Как и было предложено, вместо 0.производительность становится наравне с версией 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

Если вы скомпилируете сборку, вы увидите, что происходит под капотом. См. Также http://stackoverflow.com/questions/8021874/how-can-i-compile-to-assembly-with-gcc – Cogwheel

+8

Что произойдет, если вы измените 'push_back (Item())' на 'emplace_back()' в версия C++ 11? – Cogwheel

+8

См. Выше, что «исправляет» регрессию. Я все еще удивляюсь, почему push_back регрессирует в производительности между C++ 03 и C++ 11. – milianw

ответ

240

Я могу воспроизвести результаты на своей машине с теми вариантами, которые вы пишете в своем посте.

Однако, если я также включить link time optimization (я также передать -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). В 11 режиме генерируемого кода C++ значительно более загроможденным, чем для C++, 98 Режим и встраивание функции
void std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
терпит неудачу в режиме C++ 11 с по умолчанию inline-limit.

Это не удалось встроить эффект домино. Не потому, что эта функция вызывается (она даже не называется!), Но потому, что мы должны быть готовы: Если это называется, функция argments (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 #, 

Это хорошее и компактный цикл. Теперь давайте сравним это к тому, что из не удалось INLINE случая:

.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, потому что inlining преуспевает, у нас есть только 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,5x), и все, что мне нужно было сделать, это передать флаг -flto. Это очень хорошая сделка! ;)

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


Большой вопрос, +1!

+3

NB: 'inline' не имеет никакого отношения к функции вложения; это означает «определенный встроенный», а не «пожалуйста, включите это». Если вы хотите запросить вложение, используйте '__attribute __ ((always_inline))' или аналогичный. –

+2

@JonPurdy Не совсем, например, функции класса-члена неявно встроены. 'inline' также является запросом к компилятору, что вы хотите, чтобы функция была встроена, и, например, компилятор Intel C++, используемый для предоставления предупреждений о производительности, если он не выполнил ваш запрос. (Недавно я не проверял icc, если он все еще делает.) К сожалению, я видел, как люди перебирали свой код с помощью 'inline' и ожидали чуда. Я бы не использовал '__attribute __ ((always_inline))'; Скорее всего, разработчики компилятора лучше знают, что делать, а что нет. (Несмотря на контрпример здесь.) – Ali

+1

@JonPurdy С другой стороны, если вы определяете функцию inline *, которая не является функцией-членом класса *, то у вас нет выбора, кроме как пометить ее inline, иначе вы получите несколько определений ошибки из компоновщика. Если это то, что вы имели в виду, тогда ОК. – Ali

Смежные вопросы