Comment fonctionnent les macros probables/improbables dans le noyau Linux et quels sont leurs avantages?

j'ai fouillé dans certaines parties du noyau Linux, et j'ai trouvé des appels comme celui-ci:

if (unlikely(fd < 0))
{
    /* Do something */
}

ou

if (likely(!err))
{
    /* Do something */
}

j'ai trouvé leur définition:

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

je sais qu'ils sont pour l'optimisation, mais comment fonctionnent-ils? Et quelle diminution de performance / taille peut-on attendre de leur utilisation? Et est-ce que cela en vaut la peine (et de perdre la portabilité probablement) au moins dans le code à goulot d'étranglement (en l'espace utilisateur, bien sûr).

280
demandé sur Ezio 2008-09-21 03:04:16

10 réponses

ils sont une indication au compilateur d'émettre des instructions qui feront que la prédiction de branche 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 libre et prendra zéro cycles. D'un autre côté, si la prédiction est erronée, cela signifie que le pipeline du processeur doit être purgé et cela peut coûter plusieurs cycles. Aussi longtemps que la prédiction est correcte la plupart du temps, cela aura tendance à être bon pour la performance.

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

263
répondu 1800 INFORMATION 2018-03-04 18:37:12

ce sont des macros qui donnent des indications au compilateur sur le chemin qu'une branche peut prendre. Les macros s'étendent à des extensions spécifiques à GCC, si elles sont disponibles.

GCC utilise ceux-ci pour optimiser pour la prévision de la branche. Par exemple, si vous avez quelque chose comme

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

return x;

alors il peut restructurer ce code pour être quelque chose de plus comme:

if (!x) {
  return x;
}

dosomething();
return x;

L'avantage de ceci est que lorsque le processeur prend une branche la première fois, il y a une surabondance importante, parce qu'il peut avoir été spéculativement charger et exécuter le code plus loin en avant. Quand il détermine qu'il va prendre la branche, alors il doit invalider cela, et commencer par la cible de la branche.

la plupart des processeurs modernes ont maintenant une sorte de prédiction de branche, mais cela aide seulement quand vous avez été à travers la branche avant, et la branche est toujours dans la prédiction de cache de branche.

là un certain nombre d'autres stratégies que le compilateur et le processeur peut utiliser dans ces scénarios. Vous pouvez trouver plus de détails sur la façon dont les prédicteurs de branche à Wikipedia: http://en.wikipedia.org/wiki/Branch_predictor

65
répondu dvorak 2008-09-20 23:21:28

décomposons 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 Linux x86_64:

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

sortie:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    "151920920"x8,%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    "151920920"x1,%edx
  15:       be 00 00 00 00          mov    "151920920"x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    "151920920"x1,%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    "151920920"x0,%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    "151920920"x8,%rsp
  34:       c3                      retq

l'ordre d'instruction en mémoire était inchangé: d'abord le printf puis le puts et le retq retour.

avec __builtin_expect

remplacer if (i) par:

if (__builtin_expect(i, 0))

et nous obtenons:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    "151940920"x8,%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    "151940920"x0,%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    "151940920"x8,%rsp
  20:       c3                      retq
  21:       ba 01 00 00 00          mov    "151940920"x1,%edx
  26:       be 00 00 00 00          mov    "151940920"x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    "151940920"x1,%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 la branche comme mentionné par d'autres réponses.

donc il est fondamentalement le même que:

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 sur l'écriture d'un exemple qui court plus vite avec __builtin_expect que sans, les CPU sont vraiment intelligents ces jours-là . Mes tentatives naïves sont ici .

56

ils provoquent le compilateur à émettre les indices de branche appropriés où le matériel les supporte. Cela signifie généralement juste de tourner quelques bits dans l'instruction opcode, donc la taille du code ne changera pas. Le CPU va commencer à récupérer les instructions à partir de l'emplacement prévu, et vider le pipeline et recommencer si cela s'avère être faux lorsque la branche est atteinte; dans le cas où l'indication est correcte, cela rendra la branche beaucoup plus rapide - précisément combien plus rapide dépendra de le matériel; et combien cela affecte les performances du code dépend de la proportion du temps de l'indice est correct.

par exemple, sur un CPU PowerPC, une branche non coupée peut prendre 16 cycles, une branche correctement suggérée 8 et une branche incorrecte suggérée 24. Dans les boucles les plus profondes, une bonne indication peut faire une énorme différence.

la portabilité n'est pas vraiment un problème - probablement la définition est dans un en-tête par plate-forme; vous pouvez simplement définir "probable" et "improbable" à rien pour les plates-formes qui ne supportent pas les indices de branche statique.

6
répondu moonshadow 2008-09-20 23:11:53
long __builtin_expect(long EXP, long C);

cette construction indique au compilateur que L'expression EXP la plus probable aura la valeur C. La valeur de retour est EXP. __builtin_s'attendre à est destiné à être utilisé dans une conditionnelle expression. Dans presque tous les cas, il sera utilisé dans le contexte des expressions booléennes dans ce cas, il est beaucoup plus pratique pour définir deux macros helper:

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

ces macros peuvent alors être utilisées comme dans

if (likely(a > 1))

référence: https://www.akkadia.org/drepper/cpumemory.pdf

4
répondu Ashish Maurya 2016-11-23 13:22:05

(observation générale - autres réponses couvrent les détails)

il n'y a aucune raison de perdre la portabilité en les utilisant.

vous avez toujours la possibilité de créer un simple effet nul" inline " ou macro qui vous permettra de compiler sur d'autres plateformes avec d'autres compilateurs.

vous n'obtiendrez tout simplement pas le bénéfice de l'optimisation si vous êtes sur d'autres plateformes.

2
répondu Andrew Edgecombe 2008-09-20 23:19:44

comme dans le commentaire de Cody , cela n'a rien à voir avec Linux, mais est un indice pour le compilateur. Ce qui arrivera dépendra de l'architecture et de la version du compilateur.

Cette caractéristique particulière de Linux est quelque peu mal utilisée dans les pilotes. Comme osgx le souligne dans sémantique de l'attribut chaud , toute fonction hot ou cold appelée dans un bloc peut automatiquement indiquer que la condition est probable ou non. Par exemple, "151930920 est" marqué cold donc, c'est redondant,

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

futures versions de gcc peuvent sélectivement mettre en ligne une fonction basée sur ces conseils. Il a également été suggéré que ce n'est pas boolean , mais une note comme dans le plus probable , etc. En général, il est préférable d'utiliser un mécanisme alternatif tel que cold . Il n'y a pas de raison de l'utiliser en toute place, mais chaud chemins. Ce qu'est un compilateur fera sur une architecture peut être complètement différent sur l'autre.

2
répondu artless noise 2017-05-23 12:26:24

dans beaucoup de versions de linux, vous pouvez trouver complier.h dans /usr/linux/ , vous pouvez l'inclure pour utiliser simplement. Et une autre opinion, peu probable () est plus utile que probable (), parce que

if ( likely( ... ) ) {
     doSomething();
}

il peut être optimisé aussi bien dans de nombreux compilateurs.

et d'ailleurs, si vous voulez observer le comportement de détail du code, vous pouvez faire simplement comme suit:

test gcc-C.C objdump -d essai.o > obj.s

alors, ouvrez obj.s, tu peux trouver la réponse.

2
répondu Finaldie 2017-06-27 00:48:50

ce sont des conseils au compilateur pour générer les préfixes sur les branches. Sur x86 / x64, ils prennent un byte, donc vous obtiendrez au plus une augmentation d'un byte pour chaque branche. Quant à la performance, elle dépend entièrement de l'application -- dans la plupart des cas, le prédicteur de branche sur le processeur les ignorera, ces jours-ci.

Edit: Oublié un endroit où ils peuvent vraiment aider. Il peut permettre au compilateur de réordonner le graphique de flux de contrôle pour réduire nombre de branches prises pour la voie "probable". Cela peut avoir une amélioration marquée dans les boucles où vous vérifiez plusieurs cas de sortie.

1
répondu Cody Brocious 2008-09-20 23:07:35

ce sont des fonctions GCC pour le programmeur pour donner une indication au compilateur sur ce que la condition de branche la plus probable sera dans une expression donnée. Cela permet au compilateur de construire les instructions de branche de sorte que le cas le plus commun prenne le moins d'instructions à exécuter.

la façon dont les instructions de branche sont construites dépend de l'architecture du processeur.

1
répondu chadwick 2008-09-20 23:08:15