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

StackOverflow https://stackoverflow.com/questions/109710

Question

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’elles servent à l’optimisation, mais comment fonctionnent-elles? 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).

Était-ce utile?

La solution

Ils suggèrent au compilateur d'émettre des instructions qui feront en sorte que la prédiction de branche privilégie l'option "probable". côté 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 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 toutes les optimisations de performances de ce type, vous ne devez 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’assemblage qu’ils veulent que cela génère.

Autres conseils

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 davantage à ceci:

if (!x) {
  return x;
}

dosomething();
return x;

L'avantage de ceci 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 n’est utile que lorsque vous avez déjà parcouru la branche 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 trouverez plus de détails sur le fonctionnement des prédicteurs de branche sur Wikipedia: http://en.wikipedia.org/wiki / Branch_predictor

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    
if (__builtin_expect(i, 0))
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
0000000000000000 <main>:
   0:       48 83 ec 08             sub    
int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
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 <*>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 <*>x8,%rsp 20: c3 retq 21: ba 01 00 00 00 mov <*>x1,%edx 26: be 00 00 00 00 mov <*>x0,%esi 27: R_X86_64_32 .rodata.str1.1 2b: bf 01 00 00 00 mov <*>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>
x1,%edx 15: be 00 00 00 00 mov <*>x0,%esi 16: R_X86_64_32 .rodata.str1.1 1a: bf 01 00 00 00 mov <*>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 <*>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 <*>x8,%rsp 34: c3 retq

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

With __ builtin_expect

Remplacez maintenant si (i) par:

<*>

et nous obtenons:

<*>

Le printf (compilé pour __ printf_chk ) a été déplacé à la toute fin de la fonction, après que met 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:

<*>

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 CPU sont vraiment intelligents jours . Mes tentatives naïves sont ici . >

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 de l'indice temporel correct.

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

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 "improbable" à rien pour les plates-formes qui ne prennent pas en charge les indications de branche statique.

long __builtin_expect(long EXP, long C);

Cette construction indique au compilateur que l'expression EXP très probablement aura la valeur C. La valeur de retour est EXP. __ builtin_expect est destiné à être utilisé dans un environnement conditionnel expression. Dans presque tous les cas, sera-t-il utilisé dans le contexte d'expressions booléennes, auquel cas il est beaucoup plus pratique de définir deux macros d’aide:

#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

(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 un simple effet nul "inline". ou une macro qui vous permettra de compiler sur d’autres plateformes avec d’autres compilateurs.

Vous ne profiterez tout simplement pas de l'optimisation si vous utilisez d'autres plates-formes.

Selon le commentaire de Cody , cela n'a rien à voir avec Linux, mais constitue un indice pour 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 a été souligné dans sémantique de l'attribut hot , toute fonction hot ou à froid appelée avec dans un bloc peut automatiquement indiquer que la condition est probable ou non. . Par exemple, dump_stack () est marqué cold , ce qui est redondant,

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

versions ultérieures de gcc peuvent incorporer de manière sélective une fonction en fonction de ces indications. Il a également été suggéré que ce ne soit pas booléen , mais un score comme dans très probablement , etc. En général, il est préférable d'utiliser un mécanisme alternatif tel que . froid . 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.

Dans de nombreuses 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 procéder comme suit:

  

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

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

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.

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 sorte que le cas le plus courant utilise le moins d’instructions à exécuter.

La structure des instructions de branche dépend de l’architecture du processeur.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top