Я обнаружил интересную регрессию производительности в небольшом фрагменте 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, особенно. благодаря ходу семантики.
РЕДАКТИРОВАТЬ: Как предлагается, используя 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% )
push_back(Item())
наemplace_back()
версию C ++ 11?Ответы:
Я могу воспроизвести ваши результаты на моей машине с теми опциями, которые вы пишете в своем посте.
Однако, если я также включу оптимизацию времени соединения (я также передам
-flto
флаг в gcc 4.7.2), результаты будут идентичны:(Я компилирую ваш оригинальный код, с
container.push_back(Item());
)Что касается причин, нужно посмотреть на сгенерированный код сборки (
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
) уже должны быть в нужном месте. Это приводит к довольно грязному коду.Вот соответствующая часть сгенерированного кода для случая, когда встраивание успешно :
Это хороший и компактный цикл. Теперь давайте сравним это с ошибочным встроенным случаем:
Этот код загроможден, и в цикле происходит намного больше, чем в предыдущем случае. Перед функцией
call
(показана последняя строка) аргументы должны быть размещены соответствующим образом:Хотя это никогда не выполняется, цикл упорядочивает вещи раньше:
Это приводит к грязному коду. Если функции нет
call
из-за того, что встраивание выполнено успешно, у нас есть только 2 инструкции перемещения в цикле, и с%rsp
(указателем стека) не происходит беспорядок . Однако, если встраивание не удается, мы получаем 6 ходов, и мы много путаемся с%rsp
.Просто чтобы подтвердить мою теорию (обратите внимание
-finline-limit
), оба в режиме C ++ 11:В самом деле, если мы попросим компилятор попытаться добавить эту функцию немного сложнее, разница в производительности исчезнет.
Так какой же смысл от этой истории? Эти неудачные строки могут стоить вам дорого, и вы должны в полной мере использовать возможности компилятора: я могу только рекомендовать оптимизацию времени ссылки. Это дало значительный прирост производительности моим программам (до 2,5x), и все, что мне нужно было сделать, это пройти
-flto
флаг. Это очень хорошая сделка! ;)Однако я не рекомендую уничтожать ваш код ключевым словом inline; пусть компилятор решит что делать. (В любом случае оптимизатору разрешается использовать встроенное ключевое слово как пробел.)
Отличный вопрос, +1!
источник
inline
имеет ничего общего с встраиванием функций; это означает «определенный в строке», а не «пожалуйста, укажите это». Если вы действительно хотите попросить встроить, использовать__attribute__((always_inline))
или аналогичные.inline
это также запрос к компилятору о том, что вы хотите, чтобы функция была встроенной, и, например, компилятор Intel C ++, используемый для выдачи предупреждений о производительности, если он не выполнил ваш запрос. (Я недавно не проверял icc, если это все еще происходит.) К сожалению, я видел, как люди ломали свой кодinline
и ждали, когда произойдет чудо. Я бы не использовал__attribute__((always_inline))
; Скорее всего, разработчики компилятора лучше знают, что делать, а что нет. (Несмотря на контрпример здесь.)inline
гласит : «Спецификатор указывает реализации, что внутреннее замещение тела функции в точке вызова должно быть предпочтительнее обычного механизма вызова функции». (§7.1.2.2) Тем не менее, реализации не обязаны выполнять эту оптимизацию, так как это в значительной степени совпадение, чтоinline
функции часто оказываются хорошими кандидатами для встраивания. Так что лучше быть явным и использовать прагму компилятора.