web-dev-qa-db-fra.com

Existe-t-il un indice de compilation pour que GCC force la prédiction de branche à toujours suivre un certain chemin?

Pour les architectures Intel, existe-t-il un moyen de demander au compilateur GCC de générer un code qui force toujours la prédiction de branche dans un code particulier? Est-ce que le matériel Intel supporte même cela? Qu'en est-il des autres compilateurs ou matériels?

J'utiliserais cela en code C++ où je connais le cas que je souhaite gérer rapidement sans me soucier du ralentissement lorsque l'autre branche doit être prise, même si elle l'a récemment prise.

for (;;) {
  if (normal) { // How to tell compiler to always branch predict true value?
    doSomethingNormal();
  } else {
    exceptionalCase();
  }
}

En guise de question suivante pour Evdzhan Mustafa, le conseil peut-il simplement spécifier un conseil pour la première fois que le processeur rencontre l'instruction, toutes les prédictions de branche ultérieures fonctionnant normalement?

109
WilliamKF

La manière correcte de définir les macros probables/improbables dans C++ 11 est la suivante:

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

Lorsque ces macros ont défini cette manière:

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

Cela pourrait changer la signification des instructions if et casser le code. Considérons le code suivant:

#include <iostream>

struct A
{
    explicit operator bool() const { return true; }
    operator int() const { return 0; }
};

#define LIKELY(condition) __builtin_expect((condition), 1)

int main() {
    A a;
    if(a)
        std::cout << "if(a) is true\n";
    if(LIKELY(a))
        std::cout << "if(LIKELY(a)) is true\n";
    else
        std::cout << "if(LIKELY(a)) is false\n";
}

Et sa sortie:

if(a) is true
if(LIKELY(a)) is false

Comme vous pouvez le constater, la définition de LIKELY en utilisant !! Comme distribution vers bool rompt la sémantique de if.

Le point ici n’est pas que operator int() et operator bool() soient liés. Ce qui est une bonne pratique.

Plutôt que d'utiliser !!(x) au lieu de static_cast<bool>(x), le contexte pour conversions contextuelles C++ 11 .

21

GCC supporte la fonction __builtin_expect(long exp, long c) pour fournir ce type de fonctionnalité. Vous pouvez consulter la documentation ici .

exp est la condition utilisée et c est la valeur attendue. Par exemple dans le cas où vous voudriez

if (__builtin_expect(normal, 1))

En raison de la syntaxe maladroite, ceci est généralement utilisé en définissant deux macros personnalisées comme

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

juste pour faciliter la tâche.

Rappelez-vous que:

  1. c'est non standard
  2. un prédicteur de branche compilateur/cpu est probablement plus habile que vous pour décider de telles choses, ce qui pourrait être une micro-optimisation prématurée
80
Jack

gcc a long __builtin_expect (long exp, long c) ( c'est moi qui souligne):

Vous pouvez utiliser __builtin_expect pour fournir au compilateur des informations de prédiction de branche. En général, vous préférerez utiliser les informations de retour de profil réelles pour ce type d'arc (-fprofile-arcs), car les programmeurs sont notoirement mal à même de prédire les performances réelles de leurs programmes . Cependant, il existe des applications dans lesquelles ces données sont difficiles à collecter.

La valeur de retour est la valeur de exp, qui devrait être une expression intégrale. La sémantique de l’intégré est qu’il est prévu que exp == c. Par exemple:

if (__builtin_expect (x, 0))
   foo ();

indique que nous ne nous attendons pas à appeler foo, puisque nous prévoyons que x sera égal à zéro. Puisque vous êtes limité aux expressions intégrales pour exp, vous devez utiliser des constructions telles que

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);

lors du test de pointeur ou de valeurs en virgule flottante.

En tant que notes de documentation, vous devriez préférer utiliser les informations de profil actuelles et cet article en montre un exemple pratique et comment, dans leur cas, cela constitue au moins une amélioration par rapport à l'utilisation de __builtin_expect. Voir aussi Comment utiliser les optimisations guidées par profil dans g ++? .

Nous pouvons également trouver un article pour les débutants en noyau Linux sur les macros kernales probable () et peu probable () qui utilise cette fonctionnalité:

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

Noter la !! utilisé dans la macro, nous pouvons trouver l'explication de cela dans Pourquoi utiliser !! (condition) au lieu de (condition)? .

Le fait que cette technique soit utilisée dans le noyau Linux ne signifie pas qu'il soit toujours logique de l’utiliser. Nous pouvons voir à partir de cette question que j'ai récemment répondu différence entre les performances de la fonction lors du passage du paramètre sous forme de constante de temps de compilation ou variable que de nombreuses techniques d'optimisation obtenues manuellement ne fonctionnent pas dans le cas général. Nous devons analyser soigneusement le code pour comprendre si une technique est efficace. De nombreuses techniques anciennes peuvent même ne pas être pertinentes avec les optimisations de compilateur modernes.

Remarque, bien que les éléments intégrés ne soient pas portables clang prend également en charge __builtin_expect .

Également sur certains les architectures peuvent ne pas faire la différence .

42
Shafik Yaghmour

Non, il n'y en a pas. (Au moins sur les processeurs x86 modernes.)

__builtin_expect mentionné dans d'autres réponses influence la manière dont gcc organise le code d'assemblage. Cela n'influence pas directement le prédicteur de branche du processeur. Bien entendu, il y aura des effets indirects sur la prédiction de branche causés par réorganiser le code. Mais sur les processeurs x86 modernes, aucune instruction n'indique à la CPU "de supposer que cette branche est/n'est pas prise".

Voir cette question pour plus de détails: Prévision de branche de préfixe Intel x86 0x2E/0x3E réellement utilisée?

Pour être clair, __builtin_expect et/ou l’utilisation de -fprofile-arcs can améliorer les performances de votre code, en donnant des indications au prédicteur de branche par la disposition du code (voir Optimisations des performances de x86-64 Assembly - Alignement et prédiction de branche =), et également améliorer le comportement du cache en maintenant le code "improbable" à l'écart du code "probable".

38
Artelius

Comme les autres réponses l'ont bien suggéré, vous pouvez utiliser __builtin_expect pour indiquer au compilateur comment organiser le code d'assemblage. Comme les documents officiels le soulignent, dans la plupart des cas, l'assembleur intégré à votre cerveau ne sera pas aussi performant que celui fabriqué par l'équipe GCC. Il est toujours préférable d'utiliser les données de profil réelles pour optimiser votre code plutôt que de deviner.

Dans le même ordre d'idées, mais pas encore mentionné, il existe un moyen spécifique à GCC pour forcer le compilateur à générer du code sur un chemin "à froid". Cela implique l'utilisation des attributs noinline et cold, qui font exactement ce qu'ils sonnent. Ces attributs ne peuvent être appliqués qu'aux fonctions, mais avec C++ 11, vous pouvez déclarer des fonctions lambda en ligne et ces deux attributs peuvent également être appliqués aux fonctions lambda.

Bien que cela tombe toujours dans la catégorie générale de la micro-optimisation, et donc le conseil standard s'applique - test ne devine pas - j'ai l'impression que c'est plus généralement utile que __builtin_expect. Pratiquement aucune génération du processeur x86 n'utilise d'indices de prédiction de branche ( référence ). La seule chose que vous pourrez néanmoins affecter est l'ordre du code d'assemblage. Puisque vous savez ce qu’est le code de gestion des erreurs ou le code "Edge Case", vous pouvez utiliser cette annotation pour vous assurer que le compilateur ne lui prédira jamais de branche et le liera au code "à chaud" lors de l’optimisation de la taille.

Exemple d'utilisation:

void FooTheBar(void* pFoo)
{
    if (pFoo == nullptr)
    {
        // Oh no! A null pointer is an error, but maybe this is a public-facing
        // function, so we have to be prepared for anything. Yet, we don't want
        // the error-handling code to fill up the instruction cache, so we will
        // force it out-of-line and onto a "cold" path.
        [&]() __attribute__((noinline,cold)) {
            HandleError(...);
        }();
    }

    // Do normal stuff
    ⋮
}

Mieux encore, GCC l'ignorera automatiquement en faveur du retour d'information de profil lorsqu'il est disponible (par exemple, lors de la compilation avec -fprofile-use).

Voir la documentation officielle ici: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes

15
Cody Gray

__builtin_expect peut être utilisé pour indiquer au compilateur quelle direction une branche doit suivre. Cela peut influencer la manière dont le code est généré. Les processeurs typiques exécutent le code plus rapidement séquentiellement. Donc si vous écrivez

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;

le compilateur va générer du code comme

if (x == 0) goto if1;
back1: if (y == 0) goto if2;
back2: if (z == 0) goto if3;
back3: ;
...
if1: ++count; goto back1;
if2: ++count; goto back2;
if3: ++count; goto back3;

Si votre indice est correct, cela exécutera le code sans aucune branche réellement effectuée. Il fonctionnera plus rapidement que la séquence normale, où chaque instruction if se diviserait autour du code conditionnel et exécuterait trois branches.

Les processeurs x86 plus récents ont des instructions pour les branches qui doivent être prises, ou pour les branches qui ne devraient pas être prises (il existe un préfixe d’instruction; vous n’êtes pas sûr des détails). Pas sûr que le processeur l'utilise. Ce n'est pas très utile, car la prédiction de branche gérera cela très bien. Donc, je ne pense pas que vous puissiez réellement influencer la branche prédiction.

3
gnasher729

En ce qui concerne l'OP, non, il n'y a aucun moyen dans GCC de dire au processeur de toujours supposer que la branche est prise ou non prise. Ce que vous avez, c'est __builtin_expect, ce que font les autres. De plus, je pense que vous ne voulez pas dire au processeur si la branche est prise ou non toujours. Les processeurs actuels, tels que l'architecture Intel, peuvent reconnaître des modèles relativement complexes et s'adapter efficacement.

Cependant, vous voudrez parfois contrôler si par défaut une branche est prévue ou non: lorsque vous savez que le code sera appelé "froid" en ce qui concerne les statistiques de branche.

Un exemple concret: le code de gestion des exceptions. Par définition, le code de gestion se produira exceptionnellement, mais peut-être qu’il produira une performance maximale (il peut exister une erreur critique à résoudre dès que possible), vous pouvez donc vouloir contrôler la prédiction par défaut.

Autre exemple: vous pouvez classer votre entrée et accéder au code qui gère le résultat de votre classification. S'il existe de nombreuses classifications, le processeur peut collecter des statistiques mais les perdre car la même classification ne se produit pas assez tôt et les ressources de prédiction sont consacrées au code récemment appelé. J'aimerais qu'il y ait une primitive pour dire au processeur "s'il vous plaît, ne consacrez pas de ressources de prédiction à ce code" comme vous pouvez parfois le dire "ne pas mettre en cache cela".

0
TheCppZoo