2010-02-05 10 views
6

Po prostu natknąłem się na zmianę, która wydaje się mieć sprzeczne z intuicją konsekwencje wydajności. Czy ktoś może podać wyjaśnienie tego zachowania?Dziwna różnica wydajności C++?

oryginalny kod:

for (int i = 0; i < ct; ++i) { 
    // do some stuff... 

    int iFreq = getFreq(i); 
    double dFreq = iFreq; 

    if (iFreq != 0) { 
     // do some stuff with iFreq... 
     // do some calculations with dFreq... 
    } 
} 

Podczas czyszczenia ten kod podczas „przejściu performance”, postanowiłem przenieść definicję dFreq wewnątrz bloku if, ponieważ był używany tylko wewnątrz if. Istnieje kilka obliczeń związanych z dFreq, więc nie wyeliminowałem go całkowicie, ponieważ pozwala to zaoszczędzić na kosztach wielu konwersji w czasie rzeczywistym z int do double. Nie spodziewałem się żadnej różnicy w wydajności, lub jeśli w ogóle jakiejkolwiek, nieznacznej poprawy. Jednak wydajność spadła o prawie 10%. Zmierzyłem to wiele razy i jest to rzeczywiście zmiana, którą wprowadziłem. Pokazany powyżej fragment kodu wykonuje się wewnątrz kilku innych pętli. Dostaję bardzo spójne przebiegi między przebiegami i mogę zdecydowanie potwierdzić, że zmiana, którą opisuję, zmniejsza wydajność o ~ 10%. Oczekuję, że wydajność wzrośnie, ponieważ konwersja int do double nastąpi tylko wtedy, gdy iFreq != 0.

kod Chnaged:

for (int i = 0; i < ct; ++i) { 
    // do some stuff... 

    int iFreq = getFreq(i); 

    if (iFreq != 0) { 
     // do some stuff with iFreq... 
     double dFreq = iFreq; 
     // do some stuff with dFreq... 
    } 
} 

Może ktoś to wyjaśnić? Używam VC++ 9.0 z/O2. Chcę tylko zrozumieć, o czym tutaj nie mówię.

+1

Otrzymasz 10% zwrotu, gdy ją cofniesz? – GManNickG

+0

Na podstawie podanych informacji nie można na nie odpowiedzieć, imo. –

+4

Można porównać wygenerowany kod zespołu. ASM może pokazywać więcej różnic niż kod wysokiego poziomu. – Codism

Odpowiedz

7

Należy umieścić konwersję do dFreq natychmiast wewnątrz if() przed robi obliczeń z iFreq. Konwersja może być wykonywana równolegle z obliczeniami całkowitymi, jeśli instrukcja znajduje się dalej w kodzie.Dobry kompilator może być w stanie popchnąć go dalej, a niezbyt dobry może po prostu zostawić go tam, gdzie upadnie. Ponieważ przeniosłeś ją po obliczeniach całkowitych, może nie zostać uruchomiona równolegle z kodem całkowitym, co prowadzi do spowolnienia. Jeśli działa równolegle, może to oznaczać niewielką poprawę lub brak poprawy w zależności od procesora (wydawanie instrukcji FP, której wynik nigdy nie zostanie użyty, będzie miało niewielki wpływ na oryginalną wersję).

Jeśli naprawdę chcesz, aby zwiększyć wydajność, liczba ludzi zrobili odniesienia i pozycjonowanie następujące kompilatory w tej kolejności:

1) ICC - Intel kompilatora 2) GCC - dobre drugie miejsce 3) Wygenerowany przez MSVC kod może być dość słaby w porównaniu do innych.

Możesz także spróbować -O3, jeśli go masz.

+0

Te diagramy pokazujące liczbę całkowitą i jednostki wykonawcze FP jasno pokazują, że uruchomienie kombinacji typów instrukcji sprawi, że rzeczy będą wykonywane szybciej. Możesz spróbować przeplatać linie kodu iFreq i dFreq. – phkahler

+0

@STingRaySC: Niektórzy mogą! Niestety, trudno jest zrobić dobrze. Chociaż ludzie koncentrują się ostatnio na MSVC i GCC (i LLVM), istnieje * wiele * kompilatorów C++. Wspomniano również o Intel; i http://comeaucomputing.com/ warto się przyjrzeć. Zobacz także http://www.infoq.com/presentations/click-crash-course-modern-hardware. –

+0

Comeau kompiluje twoje C++ do C; nie wygeneruje dla ciebie zespołu. W rezultacie nie może wykonywać tego rodzaju transformacji. – MSalters

6

Być może wynik getFreq jest przechowywany wewnątrz rejestru w pierwszym przypadku i zapisywany do pamięci w drugim przypadku? Może się również zdarzyć, że spadek wydajności ma związek z mechanizmami CPU jako przepuszczaniem potoków i/lub prognozowaniem rozgałęzień. Można sprawdzić wygenerowany kod zespołu.

2

Może kompilator optymalizuje go, przyjmując definicję poza pętlą for. kiedy umieścisz go, jeśli optymalizacje kompilatora tego nie robią.

1

Istnieje prawdopodobieństwo, że ta zmiana spowodowała, że ​​kompilator wyłączył niektóre optymalizacje. Co się stanie, jeśli przeniesiesz deklaracje powyżej pętli?

3

Spróbuj przenieść definicję dFreq poza pętli for ale zachować zadanie wewnątrz pętli for/if bloku.

Być może tworzenie dFreq na stosie co pętli, wewnątrz if, powoduje problem (chociaż kompilator powinien się tym zająć). Być może regresja w kompilatorze, jeśli dFreq var jest w czterech pętlach utworzonych raz, wewnątrz wewnątrz, jeśli jest tworzony za każdym razem.

double dFreq; 
int iFreq; 
for (int i = 0; i < ct; ++i) 
{ 
    // do some stuff... 

    iFreq = getFreq(i); 

    if (iFreq != 0) 
    { 
     // do some stuff with iFreq... 
     dFreq = iFreq; 
     // do some stuff with dFreq... 
    } 
} 
1

Łatwo się dowiedzieć. Po prostu weź 20 stackshots powolnej wersji i szybkiej wersji. W wersji powolnej zobaczysz na około dwóch ujęciach, co robi, że nie robi się w szybkiej wersji. Zobaczysz subtelną różnicę w tym, gdzie zatrzymuje się w języku asemblerowym.

4

To mi wygląda na straganie rurociągu

int iFreq = getFreq(i); 
    double dFreq = iFreq; 

    if (iFreq != 0) { 

pozwala na konwersję podwoi się zdarzyć równolegle z innym kodem od dFreq nie jest używany natychmiast. daje kompilatorowi coś pomiędzy przechowywaniem iFreq i używaniem go, więc ta konwersja jest najprawdopodobniej "wolna" .

Ale

int iFreq = getFreq(i); 

if (iFreq != 0) { 
    // do some stuff with iFreq... 
    double dFreq = iFreq; 
    // do some stuff with dFreq... 
} 

Może być uderzenie stoisko przechowywać/referencyjny po konwersji do podwojenia ponieważ rozpoczęciem korzystania z podwójną wartość od razu.

Nowoczesne procesory mogą wykonywać wiele czynności na cykl zegara, ale tylko wtedy, gdy rzeczy są niezależne. Dwie kolejne instrukcje odnoszące się do tego samego rejestru często powodują przeciągnięcie. Rzeczywista konwersja do podwójnej może zająć 3 zegary, ale wszystkie oprócz pierwszego zegara mogą być wykonane równolegle z inną pracą, pod warunkiem, że nie odnoszą się do wyniku konwersji dla instrukcji lub dwóch.

Kompilatory C++ radzą sobie całkiem nieźle z poleceniami ponownego zamawiania, aby z tego skorzystać, wygląda na to, że zmiana uległa pewnej optymalizacji.

Inną (mniej prawdopodobną) możliwością jest to, że gdy konwersja do wartości zmiennoprzecinkowej była przed gałęzią, kompilator mógł całkowicie usunąć gałąź. Kod bezgałęziowy jest często główną wygraną wydajności w nowoczesnych procesorach.

Interesujące byłoby zobaczyć, jakie instrukcje faktycznie wydał kompilator dla tych dwóch przypadków.

+0

To zależy od tego, co robi kod w oddziale. Istnieją sprytne sposoby wykonywania nie rozgałęziających się kodów dla niektórych operacji. Nie pokazaliście nam, co zrobili, więc trudno jest ustalić, czy możliwe jest rozwiązanie bezgałęziowe. –