Postanowiłem ponownie uruchomić test na własnym komputerze, używając kodu Lik32. Musiałem to zmienić, ponieważ moje okna lub kompilator uważają, że wysoka rozdzielczość to 1ms
mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -wyrażenia -g
vector<int> rand_vec(10000000);
GCC dokonało tej samej transformacji na obu oryginalnych kodach.
Zauważ, że testowane są tylko dwa pierwsze warunki, ponieważ trzeci musi zawsze być prawdziwy, GCC jest tutaj rodzajem Sherlocka.
Odwrócić
.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
To niewiele nam mówi, poza tym, że ostatni przypadek nie wymaga przewidywania oddziału.
Teraz wypróbowałem wszystkie 6 kombinacji if, pierwsze 2 są oryginalną odwrotnością i posortowane. high is> = 95, low is <20, mid is 20-94 with 10000000 iterations.
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.
Dlaczego więc kolejność jest wysoka, niska, med, a następnie szybsza (nieznacznie)
Ponieważ najbardziej nieprzewidywalny jest ostatni i dlatego nigdy nie jest uruchamiany przez predyktor gałęzi.
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.
Więc oddziały zostaną przepowiedziane, zabrane, zabrane i pozostały przy
6% + (0,94 *) 20% nieprzewidywalnych.
„Posortowane”
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
Oddziały będą przewidywane z nie zabranych, nie zabranych i Sherlocka.
25% + (0,75 *) 24% nieprzewidywalnych
Dajemy 18-23% różnicy (zmierzona różnica ~ 9%), ale musimy obliczyć cykle zamiast błędnego przewidywania%.
Załóżmy, że 17 cykli za niepoprawną karę na moim procesorze Nehalem i że każda kontrola zajmuje 1 cykl na wydanie (instrukcje 4-5), a pętla zajmuje również jeden cykl. Zależności danych to liczniki i zmienne pętlowe, ale gdy błędne prognozy staną się na przeszkodzie, nie powinno to wpłynąć na czas.
W przypadku „odwrotności” otrzymujemy czasy (powinna to być formuła stosowana w architekturze komputerowej: podejście ilościowe 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
i to samo dla „posortowane”
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% zmierzone (blisko zmierzonych!?!).
Zatem oczywiste z PO nie jest oczywiste.
Dzięki tym testom inne testy z bardziej skomplikowanym kodem lub większą liczbą zależności danych na pewno będą się różnić, dlatego zmierz swoją sprawę.
Zmiana kolejności testu zmieniła wyniki, ale mogło to wynikać z różnych wyrównań początku pętli, które najlepiej powinny być wyrównane 16 bajtów na wszystkich nowszych procesorach Intela, ale tak nie jest.