¿Cuál es la diferencia entre llamadas probables e improbables en Kernel?

11

¿Cuál es la relación entre llamadas probables e improbables en Kernel? Mientras buscaba en la fuente del núcleo, encontré estas declaraciones.

# define likely(x)      __builtin_expect(!!(x), 1)
# define unlikely(x)    __builtin_expect(!!(x), 0)

¿Alguien podría arrojar algo de luz?

Sen
fuente
Esta es realmente una pregunta de programación, más adecuada para Stack OVerflow .
Gilles 'SO- deja de ser malvado'
stackoverflow.com/questions/109710/…
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Respuestas:

14

Son sugerencias del compilador para GCC. Se usan en condicionales para decirle al compilador si es probable que se tome una rama o no. Puede ayudar al compilador a establecer el código de manera óptima para el resultado más frecuente.

Se usan así:

if (likely(some_condition)) {
  // the compiler will try and make the code layout optimal for the case
  // where some_condition is true, i.e. where this block is run
  most_likely_action();
} else {
  // this block is less frequently used
  corner_case();
}

Debe usarse con mucho cuidado (es decir, en función de los resultados reales del perfil de sucursal). Una pista incorrecta puede degradar el rendimiento (obviamente).

Algunos ejemplos de cómo se puede optimizar el código se encuentran fácilmente mediante la búsqueda GCC __builtin_expect. Esta publicación de blog sobre la optimización de gcc: __builtin_expect, por ejemplo, detalla un desmontaje con ella.

El tipo de optimizaciones que se pueden hacer es muy específico del procesador. La idea general es que, a menudo, los procesadores ejecutarán el código más rápido si no se ramifica / salta por todas partes. Cuanto más lineal sea, y cuanto más predecibles sean las ramas, más rápido se ejecutará. (Esto es especialmente cierto para procesadores con tuberías profundas, por ejemplo).

Por lo tanto, el compilador emitirá el código de modo que la rama más probable no implique un salto si eso es lo que prefiere la CPU de destino, por ejemplo.

Estera
fuente
¿Qué se entiende por unicornios ? ¿Es un término técnico o simplemente un relleno?
Sen
Quité los unicornios para evitar confusiones.
Mat
¿Podría por favor explicar que el compilador intentará hacer que el diseño del código sea óptimo para el caso ? Me gustaría saber cómo hace eso.
Sen
agregó un poco de información sobre eso. no hay una forma general de optimizar el código, todo depende mucho del procesador.
Mat
2

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

Sin esperar

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        printf("%d\n", i);
    puts("a");
    return 0;
}

Compile y descompile 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 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

El orden de las instrucciones en la memoria no cambió: primero el printfy luego putsel retqretorno.

Con esperar

Ahora reemplace 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 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  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
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

El printf(compilado a __printf_chk) se movió al final de la función, después putsy al regreso para mejorar la predicción de rama como se menciona en otras respuestas.

Entonces es básicamente lo mismo que:

int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;

Esta optimización no se realizó con -O0.

Pero buena suerte al escribir un ejemplo que funciona más rápido con __builtin_expectque sin, las CPU son realmente inteligentes en esos días . Mis ingenuos intentos están aquí .

C ++ 20 [[likely]]y[[unlikely]]

C ++ 20 ha estandarizado esas incorporaciones de C ++: /programming/51797959/how-to-use-c20s- probably -un Improved-attribute-in-if-else-statement Probablemente (un juego de palabras!) haz lo mismo.

Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功
fuente