GNU/Linux >> Tutoriels Linux >  >> Linux

Comment fonctionnent les macros probables/improbables du noyau Linux et quel est leur avantage ?

Ils suggèrent au compilateur d'émettre des instructions qui feront que la prédiction de branchement favorisera le côté "probable" d'une instruction de saut. Cela peut être une grande victoire, si la prédiction est correcte, cela signifie que l'instruction de saut est fondamentalement gratuite et ne prendra aucun cycle. D'autre part, si la prédiction est erronée, cela signifie que le pipeline du processeur doit être vidé et cela peut coûter plusieurs cycles. Tant que la prédiction est correcte la plupart du temps, cela aura tendance à être bon pour les performances.

Comme toutes ces optimisations de performances, vous ne devriez le faire qu'après un profilage approfondi pour vous assurer que le code est vraiment dans un goulot d'étranglement, et probablement compte tenu de la nature micro, qu'il est exécuté dans une boucle serrée. Généralement, les développeurs Linux sont assez expérimentés, donc j'imagine qu'ils l'auraient fait. Ils ne se soucient pas vraiment de la portabilité car ils ne ciblent que gcc, et ils ont une idée très précise de l'assembly qu'ils veulent qu'il génère.


Décompilons pour voir ce que GCC 4.8 en fait

Sans __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)
        printf("%d\n", i);
    puts("a");
    return 0;
}

Compiler et décompiler avec GCC 4.8.2 x86_64 Linux :

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

Sortie :

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

L'ordre des instructions en mémoire était inchangé :d'abord le printf puis puts et le retq retour.

Avec __builtin_expect

Remplacez maintenant if (i) avec :

if (__builtin_expect(i, 0))

et on obtient :

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>

Le printf (compilé en __printf_chk ) a été déplacé à la toute fin de la fonction, après puts et le retour pour améliorer la prédiction de branche comme mentionné par d'autres réponses.

Donc, c'est fondamentalement la même chose que :

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

Cette optimisation n'a pas été faite avec -O0 .

Mais bonne chance pour écrire un exemple qui s'exécute plus rapidement avec __builtin_expect que sans, les processeurs sont vraiment intelligents de nos jours. Mes tentatives naïves sont là.

C++20 [[likely]] et [[unlikely]]

C++20 a standardisé ces éléments intégrés C++ :Comment utiliser l'attribut probable/improbable de C++20 dans l'instruction if-else Ils feront probablement (un jeu de mots !) La même chose.


Ce sont des macros qui donnent des indications au compilateur sur la manière dont une branche peut aller. Les macros s'étendent aux extensions spécifiques à GCC, si elles sont disponibles.

GCC les utilise pour optimiser la prédiction de branche. Par exemple, si vous avez quelque chose comme ce qui suit

if (unlikely(x)) {
  dosomething();
}

return x;

Ensuite, il peut restructurer ce code pour qu'il ressemble davantage à :

if (!x) {
  return x;
}

dosomething();
return x;

L'avantage de ceci est que lorsque le processeur prend une branche pour la première fois, il y a une surcharge importante, car il peut avoir chargé et exécuté de manière spéculative du code plus loin. Lorsqu'il détermine qu'il prendra la branche, il doit l'invalider et commencer à la cible de la branche.

La plupart des processeurs modernes ont maintenant une sorte de prédiction de branche, mais cela n'aide que lorsque vous avez déjà parcouru la branche et que la branche est toujours dans le cache de prédiction de branche.

Il existe un certain nombre d'autres stratégies que le compilateur et le processeur peuvent utiliser dans ces scénarios. Vous pouvez trouver plus de détails sur le fonctionnement des prédicteurs de branche sur Wikipedia :http://en.wikipedia.org/wiki/Branch_predictor


Linux
  1. Qu'est-ce qu'une Rolling Release Linux et quel est le véritable avantage de l'utiliser

  2. Quelle est la différence entre module_init et init_module dans un module du noyau Linux ?

  3. Quel est l'avantage de compiler votre propre noyau Linux ?

  4. Comment le nouveau support matériel est-il ajouté au noyau Linux ?

  5. Que font les paramètres du noyau acpi_osi=linux et acpi_backlight=vendor ?

Commande Linux Tail :qu'est-ce que c'est et comment l'utiliser

Comment vérifier la version du noyau sous Linux

Quelle est la différence entre les noyaux macOS et Linux

Noyau Linux 5.9 :nouveautés et mise à niveau

Qu'est-ce que la commande source sous Linux et comment ça marche ?

Qu'est-ce que la commande Grep sous Linux ? Pourquoi est-il utilisé et comment fonctionne-t-il ?