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).
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.
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
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.
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))
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.
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.
(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.
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 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.