Eksperymentuję z lexerem i odkryłem, że przejście z pętli while na if-statement i pętlę do-while w jednej części programu prowadzi do ~ 20% szybszego kodu, co wydawało się szalone. Wyodrębniłem różnicę w kodzie generowanym przez kompilator do tych fragmentów zestawu. Czy ktoś wie, dlaczego szybki kod jest szybszy?Dlaczego ten kod zespołu jest szybszy?
W zespole "edi" jest bieżącą pozycją tekstową, "ebx" jest końcem tekstu, a "isAlpha" jest tabelą odnośników, która ma 1, jeśli litera jest alfabetyczna, a 0 w przeciwnym razie.
Powolny Kod:
slow_loop:
00401897 cmp edi,ebx
00401899 je slow_done (4018AAh)
0040189B movzx eax,byte ptr [edi]
0040189E cmp byte ptr isAlpha (4533E0h)[eax],0
004018A5 je slow_done (4018AAh)
004018A7 inc edi
004018A8 jmp slow_loop (401897h)
slow_done:
Szybko Kod:
fast_loop:
0040193D inc edi
0040193E cmp edi,ebx
00401940 je fast_done (40194Eh)
00401942 movzx eax,byte ptr [edi]
00401945 cmp byte ptr isAlpha (4533E0h)[eax],0
0040194C jne fast_loop (40193Dh)
fast_done:
Jeśli uruchomić tylko te fragmenty zespołu przed megabajta tekst składający się tylko z literą „a”, szybki kod jest o 30% szybszy. Domyślam się, że powolny kod jest powolny z powodu błędnej interpretacji gałęzi, ale pomyślałam, że to jednorazowy koszt.
Oto program, który kiedyś przetestować oba fragmenty:
#include <Windows.h>
#include <string>
#include <iostream>
int main(int argc, char* argv[])
{
static char isAlpha[256];
for (int i = 0; i < sizeof(isAlpha); ++i)
isAlpha[i] = isalpha(i) ? 1 : 0;
std::string test(1024*1024, 'a');
const char* start = test.c_str();
const char* limit = test.c_str() + test.size();
DWORD slowStart = GetTickCount();
for (int i = 0; i < 10000; ++i)
{
__asm
{
mov edi, start
mov ebx, limit
inc edi
slow_loop:
cmp edi,ebx
je slow_done
movzx eax,byte ptr [edi]
cmp byte ptr isAlpha [eax],0
je slow_done
inc edi
jmp slow_loop
slow_done:
}
}
DWORD slowEnd = GetTickCount();
std::cout << "slow in " << (slowEnd - slowStart) << " ticks" << std::endl;
DWORD fastStart = GetTickCount();
for (int i = 0; i < 10000; ++i)
{
__asm
{
mov edi, start
mov ebx, limit
fast_loop:
inc edi
cmp edi,ebx
je fast_done
movzx eax,byte ptr [edi]
cmp byte ptr isAlpha [eax],0
jne fast_loop
fast_done:
}
}
DWORD fastEnd = GetTickCount();
std::cout << "fast in " << (fastEnd - fastStart) << " ticks" << std::endl;
return 0;
}
Wyjście z programu testu jest
slow in 8455 ticks
fast in 5694 ticks
To * jest * zwariowane - to bardzo powszechna optymalizacja dla kompilatorów. Jeśli chodzi o to, dlaczego jest szybszy, w skróconym kodzie jest o jeden mniej skoków w przeliczeniu, a skoki mają tylko ograniczoną przepustowość. – harold
profiler rejestrujący oparty na wydajności prawdopodobnie dałby najlepszą odpowiedź, ale oprócz oczywistego skoku, zgaduję, że drugi bit kodu jest szybszy, ponieważ lepiej pasuje do pamięci podręcznej kodu (jest także kilka bajtów dla pobierania i dekodowanie, ale ten nadmiar jest tutaj bez znaczenia). Przesunięcie celu skoku może być również kolejnym czynnikiem, ale trudno to powiedzieć bez adresu. – Necrolis
Brian, jaki jest twój procesor? Spójrz na http://www.agner.org/optimize/ A harold, statyczne jmps są przewidywane jak zwykle na nowoczesnych (nie-Atomowych) procesorach x86, więc nie powinny one nic kosztować. – osgx