web-dev-qa-db-fra.com

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é la définition d'eux:

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

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

318
terminus

Ils suggèrent au compilateur d'émettre des instructions qui feront en sorte que la prédiction de branche privilégie le côté "probable" d'une instruction de saut. Cela peut être un gros gain, si la prédiction est correcte, cela signifie que l'instruction de saut est fondamentalement libre et ne prendra aucun cycle. D'un autre côté, si la prédiction est fausse, cela signifie que le pipeline de processeurs doit être vidé et qu'il peut coûter plusieurs cycles. Tant que la prédiction est correcte la plupart du temps, cela aura tendance à être bon pour la performance.

Comme pour toutes les optimisations de performances, vous ne devriez le faire qu'après un profilage approfondi pour vous assurer que le code se trouve réellement dans un goulet d'étranglement, et probablement du fait de sa nature micro-économique, qu'il est exécuté en boucle serrée. En général, les développeurs Linux sont assez expérimentés, alors 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’Assemblée qu’ils souhaitent voir générée.

297
1800 INFORMATION

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

Compilez et décompilez 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 printf puis puts et le retq retour.

Avec __builtin_expect

Maintenant, remplacez if (i) par:

if (__builtin_expect(i, 0))

et nous obtenons:

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>

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

Donc, c'est fondamentalement la même chose 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é effectuée avec -O0.

Mais bonne chance pour écrire un exemple qui fonctionne plus vite avec __builtin_expect que sans, les processeurs sont vraiment intelligents ces jours-ci . Mes tentatives naïves sont ici .

Ce sont des macros qui donnent des indications au compilateur sur la direction que peut prendre une branche. Les macros sont étendues 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 à quelque chose comme:

if (!x) {
  return x;
}

dosomething();
return x;

L'avantage de cela est que, lorsque le processeur prend une branche pour la première fois, le temps système est considérable car il a pu spéculer sur le chargement et l'exécution du code. Lorsqu'il détermine qu'il prendra la branche, il doit alors 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 ne vous aide que lorsque vous avez déjà parcouru la branche auparavant, et que la branche se trouve 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

70
dvorak

Ils font en sorte que le compilateur émette les indications de branche appropriées lorsque le matériel les prend en charge. Cela signifie généralement que l'on tourne quelques bits dans l'opcode d'instruction pour que la taille du code ne change pas. Le processeur commencera à extraire les instructions à partir de l'emplacement prévu, purgera le pipeline et recommencera si cela s'avère faux lorsque la branche est atteinte; dans le cas où l'indice est correct, la branche sera beaucoup plus rapide - précisément à quelle vitesse dépendra du matériel; et dans quelle mesure cela affectera les performances du code dépendra de la proportion d'indice de temps correcte.

Par exemple, sur un processeur PowerPC, une branche non marquée peut prendre 16 cycles, une 8 correctement indiquée et une autre 24 de manière incorrecte. Dans les boucles les plus internes, une bonne indication peut faire une énorme différence.

La portabilité n'est pas vraiment un problème - on peut supposer que la définition est dans un en-tête par plate-forme; vous pouvez simplement définir "probable" et "peu probable" pour rien pour les plates-formes qui ne prennent pas en charge les indicateurs de branche statiques.

6
moonshadow
long __builtin_expect(long EXP, long C);

Cette construction indique au compilateur que l'expression EXP aura probablement la valeur C. La valeur de retour est EXP. __ builtin_expect est destiné à être utilisé dans une expression conditionnelle. Dans presque tous les cas, il sera utilisé dans le contexte d'expressions booléennes, auquel cas il est beaucoup plus pratique de définir deux macros d'assistance:

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

Ces macros peuvent ensuite être utilisées comme dans

if (likely(a > 1))

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

5
Ashish Maurya

Dans beaucoup de versions de Linux, vous pouvez trouver complier.h dans/usr/linux /, vous pouvez l’inclure pour une utilisation simple. Et un autre avis, peu probable () est plus utile que probable (), car

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

il peut également être optimisé dans de nombreux compilateurs.

Et au fait, si vous voulez observer le comportement détaillé du code, vous pouvez simplement faire comme suit:

gcc -c test.c objdump -d test.o> obj.s

Ensuite, ouvrez obj.s, vous pouvez trouver la réponse.

2
Finaldie

Selon le commentaire de Cody , cela n’a rien à voir avec Linux, mais est un indice pour le compilateur. Ce qui se passera dépendra de l’architecture et de la version du compilateur.

Cette fonctionnalité particulière sous Linux est quelque peu mal utilisée dans les pilotes. Comme osgx indique sémantique de l'attribut hot , toute fonction hot ou cold appelée dans un bloc peut automatiquement laisser entendre que la condition est probable. ou pas. Par exemple, dump_stack() est marqué cold et est donc redondant.

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

Les futures versions de gcc pourront incorporer de manière sélective une fonction basée sur ces astuces. Il a également été suggéré que ce n’est pas boolean, mais un score comme dans le plus probable , etc. En général, il convient de le préférer. utilisez un mécanisme alternatif comme cold. Il n’ya aucune raison de l’utiliser ailleurs que dans les chemins chauds. Ce qu'un compilateur fera sur une architecture peut être complètement différent sur une autre.

2
artless noise

(commentaire général - les autres réponses couvrent les détails)

Il n'y a aucune raison pour que vous perdiez la portabilité en les utilisant.

Vous avez toujours la possibilité de créer une macro "inline" ou un effet nil simple qui vous permettra de compiler sur d'autres plates-formes avec d'autres compilateurs.

Vous ne bénéficierez tout simplement pas de l'optimisation si vous utilisez d'autres plates-formes.

2
Andrew Edgecombe

Ce sont des astuces permettant au compilateur de générer les préfixes d’indices sur les branches. Sur x86/x64, ils prennent un octet, vous obtiendrez donc une augmentation d'au plus un octet pour chaque branche. En ce qui concerne les performances, cela dépend entièrement de l'application - dans la plupart des cas, le prédicteur de branche du processeur les ignore, ces jours-ci.

Edit: Oublié un endroit où ils peuvent vraiment aider. Cela peut permettre au compilateur de réorganiser le graphe de contrôle afin de réduire le nombre de branches prises pour le chemin "probable". Cela peut entraîner une nette amélioration des boucles dans lesquelles vous vérifiez plusieurs cas de sortie.

1
Cody Brocious

Ce sont des fonctions GCC permettant au programmeur d'indiquer au compilateur quelle sera la condition de branche la plus probable dans une expression donnée. Cela permet au compilateur de construire les instructions de branche de manière à ce que le cas le plus courant prenne le moins d’instructions à exécuter.

La manière dont les instructions de branche sont construites dépend de l'architecture du processeur.

1
dcgibbons