Noté por primera vez en 2009 que GCC (al menos en mis proyectos y en mis máquinas) tienen la tendencia a generar un código notablemente más rápido si optimizo el tamaño ( -Os) en lugar de la velocidad ( -O2o -O3), y me he estado preguntando desde entonces por qué.
He logrado crear un código (bastante tonto) que muestra este comportamiento sorprendente y es lo suficientemente pequeño como para publicarlo aquí.
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
Si lo compilo -Os, se necesitan 0,38 s para ejecutar este programa y 0,44 s si se compila con -O2o -O3. Estos tiempos se obtienen de manera consistente y prácticamente sin ruido (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).
(Actualización: moví todo el código de ensamblaje a GitHub : hicieron que la publicación se hinchara y aparentemente agregan muy poco valor a las preguntas ya que las fno-align-*banderas tienen el mismo efecto).
Aquí está el ensamblado generado con -Osy -O2.
Desafortunadamente, mi comprensión del ensamblaje es muy limitada, por lo que no tengo idea de si lo que hice después fue correcto: agarré el ensamblaje -O2y fusioné todas sus diferencias en el ensamblaje, -Os excepto las .p2alignlíneas, resultado aquí . Este código todavía se ejecuta en 0.38s y la única diferencia es el .p2align material.
Si adivino correctamente, estos son rellenos para la alineación de la pila. De acuerdo con ¿Por qué el pad GCC funciona con NOP? se hace con la esperanza de que el código se ejecute más rápido, pero aparentemente esta optimización fue contraproducente en mi caso.
¿Es el relleno el culpable en este caso? ¿Porque y como?
El ruido que hace prácticamente imposibilita las micro optimizaciones de temporización.
¿Cómo puedo asegurarme de que tales alineaciones accidentales afortunadas / desafortunadas no interfieran cuando hago micro optimizaciones (no relacionadas con la alineación de la pila) en el código fuente C o C ++?
ACTUALIZAR:
Siguiendo la respuesta de Pascal Cuoq, jugué un poco con las alineaciones. Al pasar -O2 -fno-align-functions -fno-align-loopsa gcc, todos .p2alignse han ido del ensamblado y el ejecutable generado se ejecuta en 0.38s. De acuerdo con la documentación de gcc :
-Os habilita todas las optimizaciones de -O2 [pero] -Os deshabilita los siguientes indicadores de optimización:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
Entonces, parece un problema de (mal) alineamiento.
Todavía soy escéptico sobre -march=nativelo sugerido en la respuesta de Marat Dukhan . No estoy convencido de que no solo interfiera con este (mal) problema de alineación; No tiene absolutamente ningún efecto en mi máquina. (Sin embargo, voté por su respuesta).
ACTUALIZACIÓN 2:
Podemos sacar -Osde la foto. Los siguientes tiempos se obtienen compilando con
-O2 -fno-omit-frame-pointer0.37s-O2 -fno-align-functions -fno-align-loops0.37s-S -O2luego mover manualmente el ensamblajeadd()después dework()0.37s-O20.44s
Me parece que la distancia add()desde el sitio de la llamada es muy importante. Lo he intentado perf, pero la salida de perf staty perf reporttiene muy poco sentido para mí. Sin embargo, solo pude obtener un resultado consistente:
-O2:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
Para fno-align-*:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
Para -fno-omit-frame-pointer:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
Parece que estamos demorando la llamada add()en el caso lento.
He examinado todo lo que perf -epuede escupir en mi máquina; no solo las estadísticas que se dan arriba.
Para el mismo ejecutable, stalled-cycles-frontendmuestra una correlación lineal con el tiempo de ejecución; No noté nada más que pudiera correlacionarse tan claramente. (Comparar stalled-cycles-frontendpara diferentes ejecutables no tiene sentido para mí).
Incluí los errores de caché, ya que surgió como el primer comentario. Examiné todos los errores de caché que se pueden medir en mi máquina perf, no solo los que se dan arriba. Los errores de caché son muy muy ruidosos y muestran poca o ninguna correlación con los tiempos de ejecución.