web-dev-qa-db-fra.com

Quel est l'effet de la commande si ... sinon si les déclarations par probabilité?

Plus précisément, si j’ai une série de déclarations if...else if et que je connais d’une manière ou d’une autre la probabilité relative que chaque instruction sera évaluée à true, quelle différence de temps d’exécution ça fait de les trier par ordre de probabilité? Par exemple, devrais-je préférer ceci:

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something

pour ça?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something

Il semble évident que la version triée serait plus rapide. Toutefois, pour des raisons de lisibilité ou d’existence d’effets secondaires, il peut être judicieux de les ordonner de manière non optimale. Il est également difficile de dire si la prédiction de branche fonctionnera bien avec le processeur jusqu'à ce que vous exécutiez le code.

Donc, au cours de mes expériences avec cela, j'ai fini par répondre à ma propre question pour un cas spécifique, mais j'aimerais aussi entendre d'autres opinions/idées.

Important: cette question suppose que les instructions if peuvent être réorganisées de manière arbitraire sans avoir d’autres effets sur le comportement du programme. Dans ma réponse, les trois tests conditionnels s’excluent mutuellement et ne produisent aucun effet secondaire. Certes, si les déclarations doivent être évaluées dans un certain ordre pour obtenir un comportement souhaité, la question de l'efficacité est alors sans objet.

184
Carlton

En règle générale, la plupart des processeurs Intel, sinon tous, supposent que les branches en aval ne sont pas prises la première fois qu'elles les voient. Voir travail de Godbolt .

Après cela, la branche entre dans un cache de prédiction de branche et le comportement passé est utilisé pour informer la prédiction future de branche.

Donc, dans une boucle serrée, l’effet des erreurs d’ordre sera relativement faible. Le prédicteur de branche va apprendre quel ensemble de branches est le plus probable, et si vous avez une quantité de travail non négligeable dans la boucle, les petites différences ne feront pas grand chose.

Dans le code général, la plupart des compilateurs commandent par défaut (sans autre raison) le code machine produit à peu près comme vous l'avez commandé dans votre code. Ainsi, si les instructions sont des branches en avant lorsqu'elles échouent.

Vous devez donc classer vos branches par ordre de probabilité décroissante pour obtenir la meilleure prédiction de branche dès une "première rencontre".

Un micro-repère qui fait plusieurs boucles étroitement sur un ensemble de conditions et effectue un travail trivial va être dominé par de minuscules effets du nombre d'instructions et autres, et très peu par rapport aux problèmes de prédiction de branche relative. Donc, dans ce cas, vous devez profiler , car les règles empiriques ne seront pas fiables.

De plus, la vectorisation et de nombreuses autres optimisations s'appliquent à de très petites boucles serrées.

Ainsi, dans le code général, insérez le code le plus probable dans le bloc if, ce qui entraînera le moins grand nombre d'échecs de prédiction de branche non mis en cache. En boucle serrée, suivez la règle générale pour commencer, et si vous avez besoin d’en savoir plus, vous n’avez guère le choix que de profiler.

Naturellement, tout cela passe par la fenêtre si certains tests sont beaucoup moins chers que d’autres.

94

J'ai composé le test suivant pour chronométrer l'exécution de deux blocs if...else if différents, l'un classé par ordre de probabilité, l'autre par ordre inverse:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    long long sortedTime = 0;
    long long reverseTime = 0;

    for (int n = 0; n != 500; ++n)
    {
        //Generate a vector of 5000 random integers from 1 to 100
        random_device rnd_device;
        mt19937 rnd_engine(rnd_device());
        uniform_int_distribution<int> rnd_dist(1, 100);
        auto gen = std::bind(rnd_dist, rnd_engine);
        vector<int> Rand_vec(5000);
        generate(begin(Rand_vec), end(Rand_vec), gen);

        volatile int nLow, nMid, nHigh;
        chrono::time_point<chrono::high_resolution_clock> start, end;

        //Sort the conditional statements in order of increasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : Rand_vec) {
            if (i >= 95) ++nHigh;               //Least likely branch
            else if (i < 20) ++nLow;
            else if (i >= 20 && i < 95) ++nMid; //Most likely branch
        }
        end = chrono::high_resolution_clock::now();
        reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

        //Sort the conditional statements in order of decreasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : Rand_vec) {
            if (i >= 20 && i < 95) ++nMid;  //Most likely branch
            else if (i < 20) ++nLow;
            else if (i >= 95) ++nHigh;      //Least likely branch
        }
        end = chrono::high_resolution_clock::now();
        sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

    }

    cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}

En utilisant MSVC2017 avec/O2, les résultats montrent que la version triée est toujours environ 28% plus rapide que la version non triée. Selon le commentaire de luk32, j'ai également inversé l'ordre des deux tests, ce qui fait une différence notable (22% contre 28%). Le code a été exécuté sous Windows 7 sur un Intel Xeon E5-2697 v2. Ceci est bien sûr très spécifique à un problème et ne doit pas être interprété comme une réponse concluante.

44
Carlton

Non, vous ne devriez pas, sauf si vous êtes vraiment sûr que le système cible est affecté. Par défaut, sélectionnez la lisibilité.

Je doute fortement de vos résultats. J'ai un peu modifié votre exemple, pour que l'exécution inversée soit plus facile. Ideone montre assez systématiquement que l'ordre inverse est plus rapide, mais pas beaucoup. Sur certaines pistes, même cela est parfois retourné. Je dirais que les résultats ne sont pas concluants. colir ne signale pas non plus de différence réelle. Je pourrai consulter ultérieurement le processeur Exynos5422 sur mon odroid xu4.

Le fait est que les processeurs modernes ont des prédicteurs de branche. Il y a beaucoup de logique dédiée à la pré-extraction à la fois des données et des instructions, et les processeurs x86 modernes sont plutôt intelligents à cet égard. Certaines architectures plus minces telles que les bras ou les GPU pourraient être vulnérables à cela. Mais il dépend vraiment beaucoup du compilateur et du système cible.

Je dirais que l’optimisation des commandes de branche est assez fragile et éphémère. Ne le faites que comme une étape vraiment précise.

Code:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    //Generate a vector of random integers from 1 to 100
    random_device rnd_device;
    mt19937 rnd_engine(rnd_device());
    uniform_int_distribution<int> rnd_dist(1, 100);
    auto gen = std::bind(rnd_dist, rnd_engine);
    vector<int> Rand_vec(5000);
    generate(begin(Rand_vec), end(Rand_vec), gen);
    volatile int nLow, nMid, nHigh;

    //Count the number of values in each of three different ranges
    //Run the test a few times
    for (int n = 0; n != 10; ++n) {

        //Run the test again, but now sort the conditional statements in reverse-order of likelyhood
        {
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : Rand_vec) {
              if (i >= 95) ++nHigh;               //Least likely branch
              else if (i < 20) ++nLow;
              else if (i >= 20 && i < 95) ++nMid; //Most likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }

        {
          //Sort the conditional statements in order of likelyhood
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : Rand_vec) {
              if (i >= 20 && i < 95) ++nMid;  //Most likely branch
              else if (i < 20) ++nLow;
              else if (i >= 95) ++nHigh;      //Least likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }
        cout << endl;
    }
}
29
luk32

Juste mes 5 cents. Il semble que l'effet de la commande si les déclarations doivent dépendre de:

  1. Probabilité de chaque déclaration if.

  2. Nombre d'itérations, afin que le prédicteur de branche puisse entrer en action.

  3. Indications du compilateur probables/improbables, c’est-à-dire la disposition du code.

Pour explorer ces facteurs, j'ai comparé les fonctions suivantes:

commandé_Fils ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] < check_point) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] == check_point) // very unlikely
        s += 1;
}

reverse_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] == check_point) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] < check_point) // highly likely
        s += 3;
}

commandé_fichiers_avec_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (likely(data[i] < check_point)) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
}

reverse_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (likely(data[i] < check_point)) // highly likely
        s += 3;
}

les données

Le tableau de données contient des nombres aléatoires compris entre 0 et 100:

const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];

static void data_init(int data_sz)
{
    int i;
        srand(0);
    for (i = 0; i < data_sz * 1024; i++)
        data[i] = Rand() % RANGE_MAX;
}

Les resultats

Les résultats suivants concernent Intel i5 @ 3,2 GHz et G ++ 6.3.0. Le premier argument est le check_point (c'est-à-dire la probabilité dans %% pour l'instruction hautement probable if), le deuxième argument est data_sz (c'est-à-dire le nombre d'itérations).

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/75/4                    4326 ns       4325 ns     162613
ordered_ifs/75/8                   18242 ns      18242 ns      37931
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612
reversed_ifs/50/4                   5342 ns       5341 ns     126800
reversed_ifs/50/8                  26050 ns      26050 ns      26894
reversed_ifs/75/4                   3616 ns       3616 ns     193130
reversed_ifs/75/8                  15697 ns      15696 ns      44618
reversed_ifs/100/4                  3738 ns       3738 ns     188087
reversed_ifs/100/8                  7476 ns       7476 ns      93752
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/75/4         3165 ns       3165 ns     218492
ordered_ifs_with_hints/75/8        13785 ns      13785 ns      50574
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205
reversed_ifs_with_hints/50/4        6573 ns       6572 ns     105629
reversed_ifs_with_hints/50/8       27351 ns      27351 ns      25568
reversed_ifs_with_hints/75/4        3537 ns       3537 ns     197470
reversed_ifs_with_hints/75/8       16130 ns      16130 ns      43279
reversed_ifs_with_hints/100/4       3737 ns       3737 ns     187583
reversed_ifs_with_hints/100/8       7446 ns       7446 ns      93782

Une analyse

1. La commande est importante

Pour les itérations 4K et la probabilité (presque) à 100% d'une déclaration très appréciée, la différence est énorme: 223%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
reversed_ifs/100/4                  3738 ns       3738 ns     188087

Pour les itérations 4K et 50% de probabilité de déclaration très appréciée, la différence est d'environ 14%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
reversed_ifs/50/4                   5342 ns       5341 ns     126800

2. Le nombre d'itérations est important

La différence entre les itérations 4K et 8K pour une (presque) 100% de probabilité de déclaration très appréciée est environ deux fois (comme prévu):

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612

Mais la différence entre les itérations 4K et 8K pour une probabilité de 50% de déclaration très appréciée est de 5,5 fois:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852

Pourquoi est-ce? En raison des erreurs de prédicteur de branche. Voici les informations manquantes pour chaque cas mentionné ci-dessus:

ordered_ifs/100/4    0.01% of branch-misses
ordered_ifs/100/8    0.01% of branch-misses
ordered_ifs/50/4     3.18% of branch-misses
ordered_ifs/50/8     15.22% of branch-misses

Ainsi, sur mon i5, le prédicteur de branche échoue de manière spectaculaire pour les branches peu probables et les grands ensembles de données.

3. Conseils aider un peu

Pour les itérations 4K, les résultats sont légèrement pires pour une probabilité de 50% et un peu meilleurs pour une probabilité proche de 100%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687

Mais pour les itérations 8K, les résultats sont toujours un peu meilleurs:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/100/8                   3381 ns       3381 ns     207612
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205

Donc, les astuces aident aussi, mais juste un tout petit peu.

La conclusion générale est la suivante: toujours comparer le code, car les résultats peuvent surprendre.

J'espère que ça t'as aidé.

23

Sur la base de certaines des autres réponses ici, il semble que la seule réponse réelle est: ça dépend. Cela dépend au moins de ce qui suit (mais pas nécessairement dans cet ordre d'importance):

  • Probabilité relative de chaque branche. C'est la question initiale qui a été posée. Sur la base des réponses existantes, il semble exister certaines conditions dans lesquelles l'ordre par probabilité aide, mais cela ne semble pas toujours être le cas. Si les probabilités relatives ne sont pas très différentes, alors il est peu probable que leur ordre soit différent. Cependant, si la première condition survient 99,999% du temps et que la suivante est une fraction de ce qui reste, alors je supposons que le plus probable en premier serait bénéfique en termes de timing.
  • Coût du calcul de la condition vraie/fausse pour chaque branche. Si le coût en temps du test des conditions est vraiment élevé pour une branche par rapport à une autre, cela aura probablement un impact significatif sur le calendrier et l'efficacité. . Par exemple, considérons une condition qui prend une unité de temps à calculer (par exemple, vérifier l’état d’une variable booléenne) par rapport à une autre condition nécessitant des dizaines, des centaines, des milliers, voire des millions d’unités de temps à calculer (par exemple, le contenu de un fichier sur disque ou une requête SQL complexe sur une base de données volumineuse). En supposant que le code vérifie les conditions dans l’ordre à chaque fois, les conditions les plus rapides devraient probablement être les premières (sauf si elles dépendent d’autres conditions qui échouent d’abord).
  • Compilateur/Interprète Certains compilateurs (ou interprètes) peuvent inclure des optimisations pouvant affecter les performances (certaines d'entre elles ne sont présentes que si certaines options sont sélectionnées lors de la compilation et/ou de l'exécution). Donc, à moins de comparer deux compilations et deux exécutions de code identique sur le même système en utilisant exactement le même compilateur où la seule différence est l'ordre des branches en question, vous devrez laisser une marge de manœuvre aux variations du compilateur.
  • Système d'exploitation/Matériel Comme mentionné par luk32 et Yakk, différents processeurs ont leurs propres optimisations (tout comme les systèmes d'exploitation). Donc, les repères sont à nouveau susceptibles de varier ici.
  • Fréquence d'exécution du bloc de code Si le bloc qui inclut les branches est rarement utilisé (par exemple, une seule fois lors du démarrage), alors l'ordre dans lequel vous placez les branches n'a probablement aucune importance. Par contre, si votre code frappe ce bloc de code pendant une partie critique de votre code, la commande peut avoir beaucoup d’importance (en fonction des points de repère).

La seule façon de savoir avec certitude consiste à analyser votre cas spécifique, de préférence sur un système identique (ou très similaire) au système sur lequel le code sera finalement exécuté. S'il est prévu de fonctionner sur un ensemble de systèmes variés dotés de matériel, de systèmes d'exploitation, etc. différents, il est judicieux de procéder à une analyse comparative de plusieurs variantes pour déterminer celle qui convient le mieux. Il peut même être une bonne idée de compiler le code avec une commande sur un type de système et une autre sur un autre type de système.

Ma règle personnelle (dans la plupart des cas, en l’absence de repère) est d’ordonner en fonction:

  1. Conditions qui s'appuient sur le résultat de conditions préalables,
  2. Coût du calcul de la condition, puis
  3. Probabilité relative de chaque branche.
18
Ampersat

La façon dont je vois habituellement le problème résolu pour le code haute performance est de conserver l'ordre le plus lisible, tout en fournissant des astuces au compilateur. Voici un exemple de noyau Linux :

if (likely(access_ok(VERIFY_READ, from, n))) {
    kasan_check_write(to, n);
    res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
    memset(to + (n - res), 0, res);

Ici, l'hypothèse est que le contrôle d'accès va passer et qu'aucune erreur n'est renvoyée dans res. Essayer de réorganiser l'une ou l'autre de ces clauses if risquerait de confondre le code, mais les macros likely() et unlikely() facilitent la lisibilité en indiquant quel est le cas normal et quelle est l'exception.

L’implémentation Linux de ces macros utilise fonctionnalités spécifiques à GCC . Il semble que clang et le compilateur Intel C prennent en charge la même syntaxe, mais MSVC n’a pas cette fonctionnalité .

12
jpa

Cela dépend également de votre compilateur et de la plate-forme pour laquelle vous compilez.

En théorie, la condition la plus probable devrait faire que le contrôle saute le moins possible.

En règle générale, la condition la plus probable devrait être la première:

if (most_likely) {
     // most likely instructions
} else …

Les asm les plus populaires sont basés sur des branches conditionnelles qui sautent lorsque la condition est true. Ce code C sera probablement traduit en un tel pseudo:

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:
…

En effet, des sauts obligent le processeur à annuler le pipeline d’exécution et le décrochage, car le compteur de programme a changé (pour les architectures prenant en charge les pipelines qui sont réellement communs). Ensuite, il s’agit du compilateur, qui peut appliquer ou non des optimisations sophistiquées afin d’avoir la condition statistique la plus probable pour que le contrôle effectue moins de sauts.

6
NoImaginationGuy

J'ai décidé de relancer le test sur ma propre machine en utilisant le code Lik32. Je devais le changer à cause de mes fenêtres ou du compilateur en pensant que la haute résolution était de 1ms, en utilisant

mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -fexceptions -g

vector<int> Rand_vec(10000000);

GCC a effectué la même transformation sur les deux codes originaux.

Notez que seules les deux premières conditions sont testées car la troisième doit toujours être vraie, GCC est une sorte de Sherlock ici.

Sens inverse

.L233:
        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L219
.L293:
        mov     edx, DWORD PTR [rsp+104]
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
.L217:
        add     rax, 4
        cmp     r14, rax
        je      .L292
.L219:
        mov     edx, DWORD PTR [rax]
        cmp     edx, 94
        jg      .L293 // >= 95
        cmp     edx, 19
        jg      .L218 // >= 20
        mov     edx, DWORD PTR [rsp+96]
        add     rax, 4
        add     edx, 1 // < 20 Sherlock
        mov     DWORD PTR [rsp+96], edx
        cmp     r14, rax
        jne     .L219
.L292:
        call    std::chrono::_V2::system_clock::now()

.L218: // further down
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
        jmp     .L217

And sorted

        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L226
.L296:
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
.L224:
        add     rax, 4
        cmp     r14, rax
        je      .L295
.L226:
        mov     edx, DWORD PTR [rax]
        lea     ecx, [rdx-20]
        cmp     ecx, 74
        jbe     .L296
        cmp     edx, 19
        jle     .L297
        mov     edx, DWORD PTR [rsp+104]
        add     rax, 4
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
        cmp     r14, rax
        jne     .L226
.L295:
        call    std::chrono::_V2::system_clock::now()

.L297: // further down
        mov     edx, DWORD PTR [rsp+96]
        add     edx, 1
        mov     DWORD PTR [rsp+96], edx
        jmp     .L224

Donc, cela ne nous dit pas grand-chose, sauf que le dernier cas n'a pas besoin d'une prédiction de branche.

Maintenant, j'ai essayé toutes les 6 combinaisons de if, les 2 premiers sont l'inverse d'origine et triés. haut => 95, bas <20, moyen 20-94 avec 10000000 itérations chacun.

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

1900020, 7498968, 601012

Process returned 0 (0x0)   execution time : 2.899 s
Press any key to continue.

Alors, pourquoi l'ordre est-il haut, bas, med alors plus vite (marginalement)

Parce que le plus imprévisible est le dernier et n’est donc jamais traité par un prédicteur de branche.

          if (i >= 95) ++nHigh;               // most predictable with 94% taken
          else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
          else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.

Donc, les branches seront prédits pris, pris et reste avec

6% + (0,94 *) 20% de mauvaises prévisions.

"Trié"

          if (i >= 20 && i < 95) ++nMid;  // 75% not taken
          else if (i < 20) ++nLow;        // 19/25 76% not taken
          else if (i >= 95) ++nHigh;      //Least likely branch

Les branches seront prédites avec pas pris, pas pris et Sherlock.

25% + (0,75 *) 24% de mauvaises prévisions

Donner une différence de 18-23% (différence mesurée d'environ 9%), mais nous devons calculer des cycles au lieu de prédire à tort%.

Supposons une pénalité erronée de 17 cycles sur mon processeur Nehalem et que chaque contrôle prend 1 cycle à exécuter (4 à 5 instructions) et que la boucle nécessite un cycle également. Les dépendances de données sont les compteurs et les variables de boucle, mais une fois que les erreurs de prédiction sont résolues, cela ne devrait pas influencer le timing.

Donc, pour "inverser", nous obtenons les timings (ce devrait être la formule utilisée dans Architecture de l'ordinateur: une approche quantitative IIRC).

mispredict*penalty+count+loop
0.06*17+1+1+    (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+  (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration

et la même chose pour "trié"

0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1)  (= 0.06*4=0.24)
= 8.26

(8.26-7.24) /8.26 = 13.8% vs. ~ 9% mesuré (proche du mesuré!?!).

Donc, l'évidence du PO n'est pas évidente.

Avec ces tests, d'autres tests avec du code plus compliqué ou plus de dépendances de données seront certainement différents, alors mesurez votre cas.

Changer l'ordre du test a changé les résultats, mais cela pourrait être dû à différents alignements du début de la boucle, qui devrait idéalement être de 16 octets alignés sur tous les nouveaux processeurs Intel, mais ce n'est pas le cas dans ce cas.

4
Surt

Placez-les dans l'ordre logique de votre choix. Bien sûr, la branche peut être plus lente, mais la branche ne devrait pas être la majorité du travail de votre ordinateur.

Si vous travaillez sur une partie de code critique en termes de performances, utilisez certainement l'ordre logique, l'optimisation guidée par profil et d'autres techniques, mais pour le code général, je pense que c'est davantage un choix stylistique.

3
Jack

Si vous connaissez déjà la probabilité relative de l'instruction if-else, il est préférable d'utiliser la méthode triée pour des raisons de performance, car elle ne vérifie qu'une seule condition (la vraie).

De manière non triée, le compilateur vérifiera inutilement toutes les conditions et prendra du temps.

2
aditya rawat