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.
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.
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.
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;
}
}
Juste mes 5 cents. Il semble que l'effet de la commande si les déclarations doivent dépendre de:
Probabilité de chaque déclaration if.
Nombre d'itérations, afin que le prédicteur de branche puisse entrer en action.
Indications du compilateur probables/improbables, c’est-à-dire la disposition du code.
Pour explorer ces facteurs, j'ai comparé les fonctions suivantes:
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;
}
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;
}
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;
}
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;
}
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 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
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
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.
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é.
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):
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:
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é .
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.
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.
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.
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.