Imagine el código ensamblador que se generaría a partir de:
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
Supongo que debería ser algo como:
cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
Puede ver que las instrucciones están dispuestas en tal orden que el bar
el caso precede al foo
caso (a diferencia del código C). Esto puede utilizar mejor la canalización de la CPU, ya que un salto golpea las instrucciones ya obtenidas.
Antes de que se ejecute el salto, las instrucciones debajo de él (el bar
caso) se empujan a la tubería. Desde el foo
el caso es poco probable, saltar también es poco probable, por lo tanto, es poco probable que se rompa la tubería.
Vamos a descompilar para ver qué hace GCC 4.8 con él
Blagovest mencionó la inversión de ramas para mejorar la canalización, pero ¿realmente lo hacen los compiladores actuales? ¡Vamos a averiguarlo!
Sin __builtin_expect
#include "stdio.h"
#include "time.h"
int main() {
/* Use time to prevent it from being optimized away. */
int i = !time(NULL);
if (i)
puts("a");
return 0;
}
Compilar y descompilar con GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Salida:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 75 0a jne 1a <main+0x1a>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1
15: e8 00 00 00 00 callq 1a <main+0x1a>
16: R_X86_64_PC32 puts-0x4
1a: 31 c0 xor %eax,%eax
1c: 48 83 c4 08 add $0x8,%rsp
20: c3 retq
El orden de las instrucciones en la memoria no cambió:primero el puts
y luego retq
volver.
Con __builtin_expect
Ahora reemplaza if (i)
con:
if (__builtin_expect(i, 0))
y obtenemos:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 74 07 je 17 <main+0x17>
10: 31 c0 xor %eax,%eax
12: 48 83 c4 08 add $0x8,%rsp
16: c3 retq
17: bf 00 00 00 00 mov $0x0,%edi
18: R_X86_64_32 .rodata.str1.1
1c: e8 00 00 00 00 callq 21 <main+0x21>
1d: R_X86_64_PC32 puts-0x4
21: eb ed jmp 10 <main+0x10>
El puts
se movió al final de la función, el retq
¡regresa!
El nuevo código es básicamente el mismo que:
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
Esta optimización no se realizó con -O0
.
Pero buena suerte al escribir un ejemplo que se ejecute más rápido con __builtin_expect
que sin, las CPU son realmente inteligentes en estos días. Mis intentos ingenuos están aquí.
C++20 [[likely]]
y [[unlikely]]
C++20 ha estandarizado esas funciones integradas de C++:Cómo usar el atributo probable/improbable de C++20 en la instrucción if-else Es probable que (¡un juego de palabras!) hagan lo mismo.
La idea de __builtin_expect
es decirle al compilador que normalmente encontrará que la expresión se evalúa como c, de modo que el compilador pueda optimizar para ese caso.
Supongo que alguien pensó que estaba siendo inteligente y que estaba acelerando las cosas al hacer esto.
Desafortunadamente, a menos que la situación sea muy bien entendida (es probable que no hayan hecho tal cosa), bien puede haber empeorado las cosas. La documentación incluso dice:
En general, debería preferir usar comentarios de perfil reales para esto (-fprofile-arcs
), ya que los programadores son notoriamente malos para predecir cómo funcionan realmente sus programas. Sin embargo, hay aplicaciones en las que estos datos son difíciles de recopilar.
En general, no deberías usar __builtin_expect
a menos que:
- Tienes un problema de rendimiento muy real
- Ya ha optimizado los algoritmos del sistema de forma adecuada
- Tiene datos de rendimiento para respaldar su afirmación de que un caso particular es el más probable