J'ai décidé de réexécuter le test sur ma propre machine en utilisant le code Lik32. J'ai dû le changer car mes fenêtres ou mon compilateur pensaient que la haute résolution était de 1 ms, 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 d'origine.
Notez que seules les deux premières conditions sont testées car la troisième doit toujours être vraie, GCC est ici une sorte de Sherlock.
Inverser
.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
cmp edx, 19
jg .L218
mov edx, DWORD PTR [rsp+96]
add rax, 4
add edx, 1
mov DWORD PTR [rsp+96], edx
cmp r14, rax
jne .L219
.L292:
call std::chrono::_V2::system_clock::now()
.L218:
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:
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é les 6 combinaisons de if, les 2 premiers sont l'inverse d'origine et triés. haut est> = 95, bas est <20, milieu est 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, moyen puis plus rapide (marginalement)
Parce que le plus imprévisible est le dernier et n'est donc jamais exécuté via un prédicteur de branche.
if (i >= 95) ++nHigh;
else if (i < 20) ++nLow;
else if (i >= 20 && i < 95) ++nMid;
Ainsi, les branches seront prédites prises, prises et le reste avec
6% + (0,94 *) 20% de faux pronostics.
"Trié"
if (i >= 20 && i < 95) ++nMid;
else if (i < 20) ++nLow;
else if (i >= 95) ++nHigh;
Les branches seront prédites avec non prises, non prises et Sherlock.
25% + (0,75 *) 24% de faux pronostics
Donnant une différence de 18 à 23% (différence mesurée de ~ 9%), mais nous devons calculer les cycles au lieu de mal prévoir le%.
Supposons que 17 cycles de pénalité de mauvaise prédiction sur mon processeur Nehalem et que chaque vérification prend 1 cycle à émettre (4-5 instructions) et que la boucle prend également un cycle. Les dépendances de données sont les compteurs et les variables de boucle, mais une fois que les erreurs de prédiction sont écartées, cela ne devrait pas influencer le timing.
Donc, pour "inverser", nous obtenons les horaires (cela devrait être la formule utilisée dans Computer Architecture: A Quantitative Approach 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.
La modification de l'ordre du test a changé les résultats, mais cela pourrait être dû à des alignements différents du début de la boucle qui devrait idéalement être aligné sur 16 octets sur tous les nouveaux processeurs Intel, mais ce n'est pas le cas dans ce cas.