GNU/Linux >> Tutoriales Linux >  >> Linux

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

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

Linux
  1. ¿Qué es el truco LD_PRELOAD?

  2. ¿Cuál es el significado de *nix?

  3. ¿Cuál es el significado de Advertencia:vincular la biblioteca compartida con la biblioteca estática no es portátil?

  4. ¿Cuál es el concepto de vruntime en CFS?

  5. ¿Cuál es el uso de $# en Bash?

¿Qué es el Shell en Linux?

iptables vs nftables:¿Cuál es la diferencia?

¿Qué es el comando matar en Linux?

¿Qué es la vulnerabilidad de Logjam?

¿Cuál es la contraseña predeterminada de la pantalla?

¿Qué es el enlazador de oro?