He encontrado una regresión de rendimiento interesante en un pequeño fragmento de C ++, cuando habilito 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 (versión preliminar) y C ++ 03 obtengo:
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 habilitado por otro lado, el rendimiento se degrada 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% )
¿Alguien puede explicar esto? Hasta ahora, mi experiencia fue que el STL se acelera al habilitar C ++ 11, especialmente. gracias a mover semántica.
EDITAR: Como se sugirió, el uso container.emplace_back();
del rendimiento se equipara con la versión C ++ 03. ¿Cómo puede la versión C ++ 03 lograr lo mismo 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())
aemplace_back()
la versión C ++ 11?Respuestas:
Puedo reproducir tus resultados en mi máquina con esas opciones que escribes en tu publicación.
Sin embargo, si también habilito la optimización del tiempo de enlace (también paso el
-flto
indicador a gcc 4.7.2), los resultados son idénticos:(Estoy compilando su código original, con
container.push_back(Item());
)En cuanto a las razones, uno debe mirar el código de ensamblaje generado (
g++ -std=c++11 -O3 -S regr.cpp
). En modo C ++ 11, el código generado está significativamente más desordenado que en el modo C ++ 98 y la función de alineaciónvoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
falla en el modo C ++ 11 con el valor predeterminado
inline-limit
.Esta falla en línea tiene un efecto dominó. No porque se llame a esta función (¡ni siquiera se llama!) Sino porque tenemos que estar preparados: si se llama, los argumentos de la función (
Item.a
yItem.b
) ya deben estar en el lugar correcto. Esto lleva a un código bastante desordenado.Aquí está la parte relevante del código generado para el caso donde la alineación tiene éxito :
Este es un bucle agradable y compacto. Ahora, comparemos esto con el de caso en línea fallido :
Este código está abarrotado y hay muchas más actividades en el bucle que en el caso anterior. Antes de la función
call
(se muestra la última línea), los argumentos deben colocarse adecuadamente:Aunque esto nunca se ejecuta realmente, el bucle organiza las cosas antes:
Esto lleva al código desordenado. Si no hay función
call
porque la alineación tiene éxito, solo tenemos 2 instrucciones de movimiento en el bucle y no hay ningún problema con el%rsp
(puntero de pila). Sin embargo, si la alineación falla, obtenemos 6 movimientos y nos metemos mucho con el%rsp
.Solo para corroborar mi teoría (note el
-finline-limit
), tanto en modo C ++ 11:De hecho, si le pedimos al compilador que intente un poco más difícil de incorporar esa función, la diferencia en el rendimiento desaparece.
Entonces, ¿cuál es la conclusión de esta historia? Las fallas en las líneas pueden costarle mucho y debe aprovechar al máximo las capacidades del compilador: solo puedo recomendar la optimización del tiempo de enlace. Dio un aumento significativo en el rendimiento de mis programas (hasta 2.5x) y todo lo que necesitaba hacer era pasar el
-flto
bandera. Eso es un buen trato! ;)Sin embargo, no recomiendo tirar a la basura su código con la palabra clave en línea; Deje que el compilador decida qué hacer. (De todos modos, el optimizador puede tratar la palabra clave en línea como espacio en blanco).
Gran pregunta, +1!
fuente
inline
no tiene nada que ver con la función en línea; significa "en línea definida" no "por favor, en línea esto". Si realmente desea solicitar la alineación, use__attribute__((always_inline))
o similar.inline
también es una solicitud para el compilador de que le gustaría que la función esté integrada y, por ejemplo, el compilador Intel C ++ solía dar advertencias de rendimiento si no cumplía con su solicitud. (No he revisado icc recientemente si todavía lo hace). Desafortunadamente, he visto a personas destrozando su códigoinline
y esperando que ocurra un milagro. No lo usaría__attribute__((always_inline))
; Lo más probable es que los desarrolladores del compilador sepan mejor qué incluir y qué no. (A pesar del contraejemplo aquí.)inline
especificador indica a la implementación que la sustitución en línea del cuerpo de la función en el punto de llamada debe preferirse al mecanismo habitual de llamada a la función". (§7.1.2.2) Sin embargo, no se requieren implementaciones para realizar esa optimización, ya que es una coincidencia que lasinline
funciones a menudo sean buenas candidatas para la inclusión. Por lo tanto, es mejor ser explícito y usar un pragma compilador.