¿Cuál es la ventaja de la __builtin_pect de GCC en declaraciones if else?

Encontré un #define en el que usan __builtin_expect .

La documentación dice:

Función incorporada: long __builtin_expect (long exp, long c)

Puede usar __builtin_expect para proporcionar al comstackdor información de predicción de bifurcación. En general, debería preferir utilizar comentarios de perfil reales para esto ( -fprofile-arcs ), ya que los progtwigdores son notoriamente malos a la hora de predecir cómo funcionan realmente sus progtwigs. Sin embargo, hay aplicaciones en las cuales estos datos son difíciles de recostackr.

El valor de retorno es el valor de exp , que debe ser una expresión integral. La semántica del built-in es que se espera que exp == c . Por ejemplo:

  if (__builtin_expect (x, 0)) foo (); 

indicaría que no esperamos llamar a foo , ya que esperamos que x sea ​​cero.

Entonces, ¿por qué no usar directamente?

 if (x) foo (); 

en lugar de la syntax complicada con __builtin_expect ?

Imagine el código de ensamblaje que se generaría a partir de:

 if (__builtin_expect(x, 0)) { foo(); ... } else { bar(); ... } 

Supongo que debería ser algo así 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 un orden tal que la caja de bar precede al caso foo (en oposición al código C). Esto puede utilizar mejor la tubería de la CPU, ya que un salto supera las instrucciones ya recuperadas.

Antes de que se ejecute el salto, las instrucciones debajo (la caja de la bar ) se envían a la tubería. Dado que el caso foo es poco probable, es poco probable que se salte demasiado, por lo tanto, es poco probable que se rompa la tubería.

La idea de __builtin_expect es decirle al comstackdor que normalmente encontrará que la expresión se evalúa como c, de modo que el comstackdor puede optimizar para ese caso.

Supongo que alguien pensó que estaban siendo inteligentes y que estaban acelerando las cosas al hacer esto.

Desafortunadamente, a menos que la situación se entienda muy bien (es probable que no hayan hecho tal cosa), bien podría haber empeorado las cosas. La documentación incluso dice:

En general, debería preferir utilizar comentarios de perfil reales para esto ( -fprofile-arcs ), ya que los progtwigdores son notoriamente malos a la hora de predecir cómo funcionan realmente sus progtwigs. Sin embargo, hay aplicaciones en las cuales estos datos son difíciles de recostackr.

En general, no deberías estar utilizando __builtin_expect menos que:

  • Tienes un problema de rendimiento muy real
  • Ya ha optimizado los algoritmos en el sistema de forma adecuada
  • Tiene datos de rendimiento para respaldar su afirmación de que un caso particular es el más probable

Vamos a descomstackr para ver qué hace GCC 4.8 con él

Blagovest mencionó la inversión de sucursales para mejorar la canalización, pero ¿realmente lo hacen los comstackdores actuales? ¡Vamos a averiguar!

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; } 

Comstack y descomstack con GCC 4.8.2 x86_64 Linux:

 gcc -c -O3 -std=gnu11 main.c objdump -dr main.o 

Salida:

 0000000000000000 
: 0: 48 83 ec 08 sub $0x8,%rsp 4: 31 ff xor %edi,%edi 6: e8 00 00 00 00 callq b 7: R_X86_64_PC32 time-0x4 b: 48 85 c0 test %rax,%rax e: 75 0a jne 1a 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 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 se modificó: primero las puts y luego retq regresan.

Con __builtin_expect

Ahora reemplace if (i) con:

 if (__builtin_expect(i, 0)) 

y obtenemos:

 0000000000000000 
: 0: 48 83 ec 08 sub $0x8,%rsp 4: 31 ff xor %edi,%edi 6: e8 00 00 00 00 callq b 7: R_X86_64_PC32 time-0x4 b: 48 85 c0 test %rax,%rax e: 74 07 je 17 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 1d: R_X86_64_PC32 puts-0x4 21: eb ed jmp 10

Las puts se movieron al final de la función, ¡el retorno de retq !

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 ejecuta más rápido con __builtin_expect que sin, las CPU son realmente inteligentes en esos días . Mis ingenuos bashs están aquí .

Bueno, como se dice en la descripción, la primera versión agrega un elemento predictivo a la construcción, diciéndole al comstackdor que la twig x == 0 es la más probable, es decir, es la twig que tomará con más frecuencia su progtwig.

Con esto en mente, el comstackdor puede optimizar el condicional para que requiera la menor cantidad de trabajo cuando se cumple la condición esperada, a expensas de tal vez tener que trabajar más en caso de una situación inesperada.

Observe cómo se implementan los condicionales durante la fase de comstackción, y también en el ensamblaje resultante, para ver cómo una twig puede tener menos trabajo que la otra.

Sin embargo, solo esperaría que esta optimización tenga un efecto notable si el condicional en cuestión es parte de un circuito interno cerrado que se llama mucho , ya que la diferencia en el código resultante es relativamente pequeña. Y si lo optimizas al revés, puedes disminuir tu rendimiento.

No veo ninguna de las respuestas que aborden la pregunta que creo que estaba haciendo, parafraseada:

¿Hay alguna manera más portátil de insinuar predicción de bifurcación al comstackdor?

El título de tu pregunta me hizo pensar en hacerlo de esta manera:

 if ( !x ) {} else foo(); 

Si el comstackdor supone que ‘verdadero’ es más probable, podría optimizarse para no llamar a foo() .

El problema aquí es que, en general, usted no sabe lo que el comstackdor asumirá, por lo que cualquier código que use este tipo de técnica deberá medirse cuidadosamente (y posiblemente monitorearse a lo largo del tiempo si el contexto cambia).