Vous êtes victime d'un échec de prédiction de branche .
Qu'est-ce que la prédiction de branche?
Considérons une jonction ferroviaire:
Image par Mecanismo, via Wikimedia Commons. Utilisé sous la licence CC-By-SA 3.0 .
Maintenant, pour les besoins de l'argument, supposons que cela remonte aux années 1800 - avant les communications longue distance ou radio.
Vous êtes l'opérateur d'un carrefour et vous entendez arriver un train. Vous n'avez aucune idée de la direction à prendre. Vous arrêtez le train pour demander au conducteur dans quelle direction il veut. Et puis vous réglez le commutateur de manière appropriée.
Les trains sont lourds et ont beaucoup d'inertie. Ils mettent donc une éternité à démarrer et à ralentir.
Y a-t-il une meilleure façon? Vous devinez dans quelle direction le train ira!
- Si vous avez bien deviné, cela continue.
- Si vous vous trompez, le capitaine s'arrête, recule et vous crie dessus pour actionner l'interrupteur. Ensuite, il peut redémarrer sur l'autre chemin.
Si vous devinez à chaque fois , le train n'aura jamais à s'arrêter.
Si vous vous trompez trop souvent , le train passera beaucoup de temps à s'arrêter, à reculer et à redémarrer.
Considérons une instruction if: au niveau du processeur, il s'agit d'une instruction de branchement:
Vous êtes un processeur et vous voyez une branche. Vous n'avez aucune idée de la direction que cela prendra. Que faire? Vous arrêtez l'exécution et attendez que les instructions précédentes soient terminées. Ensuite, vous continuez sur le bon chemin.
Les processeurs modernes sont compliqués et ont de longs pipelines. Ils mettent donc une éternité à «s'échauffer» et à «ralentir».
Y a-t-il une meilleure façon? Vous devinez dans quelle direction ira la succursale!
- Si vous avez bien deviné, vous continuez à exécuter.
- Si vous vous êtes trompé, vous devez rincer le pipeline et revenir à la branche. Ensuite, vous pouvez redémarrer l'autre chemin.
Si vous devinez à chaque fois , l'exécution ne devra jamais s'arrêter.
Si vous vous trompez trop souvent , vous passez beaucoup de temps à caler, à reculer et à redémarrer.
Ceci est une prédiction de branche. J'avoue que ce n'est pas la meilleure analogie car le train pourrait simplement signaler la direction avec un drapeau. Mais dans les ordinateurs, le processeur ne sait pas dans quelle direction ira une branche jusqu'au dernier moment.
Alors, comment devineriez-vous stratégiquement pour minimiser le nombre de fois que le train doit reculer et descendre l'autre chemin? Vous regardez l'histoire passée! Si le train part à 99% du temps, alors vous devinez parti. S'il alterne, alors vous alternez vos suppositions. Si cela va dans un sens toutes les trois fois, vous devinez la même chose ...
En d'autres termes, vous essayez d'identifier un modèle et de le suivre. C'est plus ou moins comment fonctionnent les prédicteurs de branche.
La plupart des applications ont des branches bien comportées. Ainsi, les prédicteurs de branche modernes atteindront généralement des taux de réussite supérieurs à 90%. Mais face à des branches imprévisibles sans schémas reconnaissables, les prédicteurs de branche sont pratiquement inutiles.
Pour en savoir plus: article "Predicteur de branche" sur Wikipédia .
Comme laissé entendre ci-dessus, le coupable est cette instruction if:
if (data[c] >= 128)
sum += data[c];
Notez que les données sont réparties uniformément entre 0 et 255. Lorsque les données sont triées, à peu près la première moitié des itérations n'entrera pas dans l'instruction if. Après cela, ils entreront tous dans l'instruction if.
Ceci est très convivial pour le prédicteur de branche car la branche va dans le même sens plusieurs fois de suite. Même un simple compteur saturant prédira correctement la branche, à l'exception des quelques itérations après avoir changé de direction.
Visualisation rapide:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
Cependant, lorsque les données sont complètement aléatoires, le prédicteur de branche est rendu inutile, car il ne peut pas prédire des données aléatoires. Ainsi, il y aura probablement environ 50% d'erreurs de prédiction (pas mieux que des suppositions aléatoires).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
Alors, que peut-on faire?
Si le compilateur n'est pas en mesure d'optimiser la branche dans un mouvement conditionnel, vous pouvez essayer quelques hacks si vous êtes prêt à sacrifier la lisibilité pour les performances.
Remplacer:
if (data[c] >= 128)
sum += data[c];
avec:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
Cela élimine la branche et la remplace par quelques opérations au niveau du bit.
(Notez que ce hack n'est pas strictement équivalent à l'instruction if d'origine. Mais dans ce cas, il est valide pour toutes les valeurs d'entrée de data[]
.)
Repères: Core i7 920 @ 3,5 GHz
C ++ - Visual Studio 2010 - Version x64
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java - NetBeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
Observations:
- Avec la succursale: Il y a une énorme différence entre les données triées et non triées.
- Avec le Hack: il n'y a pas de différence entre les données triées et non triées.
- Dans le cas C ++, le hack est en fait un peu plus lent qu'avec la branche lorsque les données sont triées.
Une règle générale consiste à éviter la ramification dépendante des données dans les boucles critiques (comme dans cet exemple).
Mise à jour:
GCC 4.6.1 avec -O3
ou -ftree-vectorize
sur x64 est capable de générer un déplacement conditionnel. Il n'y a donc aucune différence entre les données triées et non triées - les deux sont rapides.
(Ou un peu rapide: pour le cas déjà trié, cmov
peut être plus lent, surtout si GCC le place sur le chemin critique plutôt que juste add
, en particulier sur Intel avant Broadwell où la cmov
latence est à 2 cycles: l' indicateur d'optimisation gcc -O3 rend le code plus lent que -O2 )
VC ++ 2010 est incapable de générer des mouvements conditionnels pour cette branche même sous /Ox
.
Intel C ++ Compiler (ICC) 11 fait quelque chose de miraculeux. Il échange les deux boucles , hissant ainsi la branche imprévisible à la boucle externe. Ainsi, non seulement il est immunisé contre les erreurs de prévision, mais il est également deux fois plus rapide que ce que VC ++ et GCC peuvent générer! En d'autres termes, ICC a profité de la boucle de test pour battre la référence ...
Si vous donnez au compilateur Intel le code sans branche, il le vectorise juste à droite ... et est aussi rapide qu'avec la branche (avec l'échange de boucle).
Cela montre que même les compilateurs modernes matures peuvent varier considérablement dans leur capacité à optimiser le code ...