Encontrei uma regressão interessante de desempenho em um pequeno trecho de C ++, quando habilito o 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;
}
Com g ++ (GCC) 4.8.2 20131219 (pré-lançamento) e C ++ 03, recebo:
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% )
Com o C ++ 11 ativado, por outro lado, o desempenho diminui significativamente:
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% )
Alguém pode explicar isso? Até agora, minha experiência foi que o STL fica mais rápido ao ativar o C ++ 11, esp. graças a mover semântica.
EDIT: Como sugerido, o uso container.emplace_back();
do desempenho é semelhante à versão C ++ 03. Como a versão C ++ 03 pode alcançar o mesmo 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())
paraemplace_back()
na versão C ++ 11?Respostas:
Posso reproduzir seus resultados na minha máquina com as opções que você escreve em sua postagem.
No entanto, se eu também habilitar a otimização do tempo do link (eu também passo o
-flto
sinalizador para o gcc 4.7.2), os resultados são idênticos:(Estou compilando seu código original, com
container.push_back(Item());
)Quanto aos motivos, é necessário observar o código de montagem gerado (
g++ -std=c++11 -O3 -S regr.cpp
). No modo C ++ 11, o código gerado é significativamente mais confuso do que no modo C ++ 98, e a inserção de funçãovoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
falha no modo C ++ 11 com o padrão
inline-limit
.Esta falha na linha tem um efeito dominó. Não porque essa função está sendo chamada (nem sequer é chamada!), Mas porque temos que estar preparados: Se for chamada, a função argments (
Item.a
eItem.b
) já deve estar no lugar certo. Isso leva a um código bastante confuso.Aqui está a parte relevante do código gerado para o caso em que a inclusão é bem - sucedida :
Este é um loop for agradável e compacto. Agora, vamos comparar isso com o caso inline com falha :
Esse código é confuso e há muito mais acontecendo no loop do que no caso anterior. Antes da função
call
(última linha mostrada), os argumentos devem ser colocados adequadamente:Mesmo que isso nunca seja realmente executado, o loop organiza as coisas antes:
Isso leva ao código confuso. Se não houver nenhuma função
call
porque o inlining for bem-sucedido, temos apenas 2 instruções de movimento no loop e não há problemas com o%rsp
(ponteiro da pilha). No entanto, se o inline falhar, temos 6 jogadas e mexemos muito com a%rsp
.Apenas para substanciar minha teoria (observe o
-finline-limit
), ambos no modo C ++ 11:De fato, se pedirmos ao compilador que tente um pouco mais difícil incorporar essa função, a diferença no desempenho desaparece.
Então, qual é a vantagem dessa história? As falhas de linha podem custar muito e você deve fazer uso total dos recursos do compilador: só posso recomendar a otimização do tempo do link. Isso deu um aumento significativo no desempenho dos meus programas (até 2,5x) e tudo que eu precisava fazer era passar a
-flto
bandeira. Isso é um bom negócio! ;)No entanto, eu não recomendo lixeira seu código com a palavra-chave inline; deixe o compilador decidir o que fazer. (O otimizador pode tratar a palavra-chave embutida como espaço em branco de qualquer maneira.)
Ótima pergunta, +1!
fonte
inline
não tem nada a ver com função embutida; significa "inline definido" e não "inline this". Se você realmente deseja solicitar inlining, use__attribute__((always_inline))
ou similar.inline
também é uma solicitação ao compilador de que você gostaria que a função fosse incorporada e, por exemplo, o Intel C ++ Compiler usado para emitir avisos de desempenho, se não atender à sua solicitação. (Não verifiquei o icc recentemente, se ainda o faz.) Infelizmente, tenho visto pessoas alterando seu códigoinline
e esperando o milagre acontecer. Eu não usaria__attribute__((always_inline))
; é provável que os desenvolvedores do compilador saibam melhor o que incorporar e o que não incorporar. (Apesar do contraexemplo aqui.)inline
especificador indica para a implementação que a substituição em linha do corpo da função no ponto de chamada deve ser preferida ao mecanismo usual de chamada de função". (§7.1.2.2) No entanto, as implementações não são necessárias para executar essa otimização, pois é em grande parte uma coincidência que asinline
funções geralmente sejam boas candidatas a inlining. Portanto, é melhor ser explícito e usar um pragma do compilador.