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 ( -O2
o -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 -O2
o -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 -Os
y -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 -O2
y fusioné todas sus diferencias en el ensamblaje, -Os
excepto las .p2align
lí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-loops
a gcc, todos .p2align
se 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=native
lo 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 -Os
de la foto. Los siguientes tiempos se obtienen compilando con
-O2 -fno-omit-frame-pointer
0.37s-O2 -fno-align-functions -fno-align-loops
0.37s-S -O2
luego mover manualmente el ensamblajeadd()
después dework()
0.37s-O2
0.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 stat
y perf report
tiene 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 -e
puede escupir en mi máquina; no solo las estadísticas que se dan arriba.
Para el mismo ejecutable, stalled-cycles-frontend
muestra una correlación lineal con el tiempo de ejecución; No noté nada más que pudiera correlacionarse tan claramente. (Comparar stalled-cycles-frontend
para 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.