2015-02-23 21 views
5

Mój kod będzie przemierzał drzewo binarne w sposób rekursywny. Robiąc to mam kilka parametrów, które muszę kontrolować. Tak więc moja funkcja wygląda następująco:Czy wiele parametrów funkcji rekurencyjnej spowoduje problemy z wydajnością?

FindPoints(int leftchild, int rightchild, int ly_index, int uy_index, int bit, int nodepos, int amount, int level); 

Nazywa się to wiele razy. Czy wydajność mojego programu osiągnie celność ze względu na ilość parametrów?

+1

Cóż, to będzie oczywiście trwać dłużej niż gdybyś miał mniej - o ile parametry zdarzyć mapować tych samych rejestrów rozmówcy i odbierającym (mało prawdopodobne). Ale czy te parametry nie są niezbędne do prawidłowego działania twojej funkcji? –

+2

A co z definicją struktury i przekazaniem jej przez odniesienie? –

+0

@PaoloM Jaki wpływ będzie miało na wydajność?Jestem bardzo zainteresowany :) – Mads

Odpowiedz

3

Nie ma znaczącego wpływu na wydajność. Nie jest ważne. Jeśli potrzebujesz ekstremalnej wydajności, powinieneś zrobić to w iteracji

Ale jest to brudny kod. Powinieneś spróbować hermetyzować argumenty w strukturach lub klasie. Jego bezpieczniejsze i łatwiejsze w utrzymaniu

+1

Bazując na moim benchmarku, bardzo boli wydajność. Na jakiej podstawie twierdzicie, że jest inaczej? –

7

Proces podczas rekursji jest:

  1. Przydzielenie miejsca dla parametrów na stosie. Zwykle dodawanie wartości do rejestru wskaźnika stosu.
  2. Skopiuj wartości zmiennych na stos. Zależy od obiektów lub wartości .
  3. Funkcja połączenia. Może to spowodować przepełnienie pamięci podręcznej instrukcji procesora: .
  4. Po zakończeniu funkcji wskaźnik stosu jest przywracany przez odjęcie wartości .
  5. Powrót z wywołania funkcji; może powodować opróżnianie pamięci podręcznej instrukcji.

Głównym problemem nie jest wydajność, ale głębokość rekursji i rozmiar stosu. Rekursja, która wykracza poza ograniczenia stosu, nazywa się defektem stosu Stack Overflow.

Rozwiązanie iteracyjne może być szybsze, ponieważ kompilator może zoptymalizować pętlę. Optymalizacja wywołań rekursywnych jest trudniejsza dla optymalizacji kompilatora.

Nawiasem mówiąc, na nowoczesnych procesorach, najgorszy przypadek czasu wywołania rekurencyjnego jest krótszy niż 1 milisekunda, zwykle około nanosekundowej rozdzielczości. Więc próbujesz wycisnąć nanosekundy z programu. Nie bardzo dobry zwrot z inwestycji (ROI).

5

Wydajność zależy od wielu czynników; najlepiej spróbować w jedną stronę, wypróbować drugą i porównać. Poniżej przedstawiamy kilka ogólnych uwag, które mogą pomóc w zrozumieniu tego, co się dzieje:

  • Jeśli twoja funkcja wykonuje dużo pracy, czas stracony na wywoływanie funkcji nie będzie znaczący.
  • Jeśli twoja funkcja przeważnie przechodzi parametry niezmienione, powinieneś zdecydowanie pomyśleć o refaktoryzacji swojego kodu. Jeśli wywoła się sam ze wszystkimi (lub prawie wszystkimi) parametrami mającymi różne wartości, nie da się poprawić kodu - jest zbyt skomplikowany.
  • Wykonywanie połączeń funkcji zależy od konwencji wywołań. Kompilatory zazwyczaj przekazują kilka pierwszych parametrów w rejestrach (bardzo szybko), a pozostałe na stosie (wolniej). Możesz ustawić małą liczbę parametrów (2 dla fastcall; 4 dla ARM - tylko dwa przykłady, które znam), tak, że wszystkie pasują do rejestrów.

Aby rozwinąć drugi punkt - jeśli twoja funkcja nie zmienia większości parametrów, każde wywołanie skopiuje te parametry wokół stosu - jest to absolutnie bezużyteczne działanie dla komputera. Oprócz marnowania czasu, marnuje on również pamięć podręczną danych (co prowadzi do spowolnienia, co jest szczególnie nieprzyjemne, ponieważ nie można go nawet przypisać do żadnego kodu) i może spowodować przepełnienie stosu (lub może nie, w zależności od systemu operacyjnego) .

Jednym ze sposobów, aby poprawić swój kod w tej sytuacji jest: za pomocą struct który przechowuje wszystkie parametry bez zmian, a przechodzącą wskaźnik/odniesienia do niego:

struct DataForFindPoints 
{ 
    int ly_index; 
    int uy_index; 
    int bit; 
    int nodepos; 
    int amount; 
    int level; 
}; 

FindPoints(int leftchild, int rightchild, const DataForFindPoints& data); 

Albo (sposób zorientowany obiektowo): zrobić class, który ma FindPoints jako funkcję składową i wszystkie niezmienione parametry jako pola.

4

Krótka odpowiedź

Pod Windows kompilacji z Visual Studio 2010 w trybie uwalniania kierowania platformy x64, przechodząc rozpakowanego argumentów jest znacznie wolniejszy niż przekazując jeden struct przez odniesienie lub nawet przez wartość.

Wyniki śledzić:

Multi result = 0; multi iterations = 10000 
Ref result = 0; ref iterations = 10000 
Value result = 0; value iterations = 10000 

--------------------------------------------------- 
Timer "multi args": 
Total time = 0.387886 

------------------------------------------ 
Timer "struct by reference": 
Total time = 0.0679177 

------------------------------------------ 
Timer "struct by value": 
Total time = 0.143382 

Obserwacja

Bardziej czynność wykonuje obliczenia w swoim ciele, tym mniej napowietrznych kopia będzie bolało wydajność. W rzeczywistości przetestowałem funkcję, która wykonuje tylko kilka dodatków i jeden podział.

Niektóre dane teraz

Mam zdefiniowane struct zawierający wszystkie parametry

struct Args{ 
    int leftchild; 
    int rightchild; 
    int ly_index; 
    int uy_index; 
    int bit; 
    int nodepos; 
    int amount; 
    int level; 

    Args(int l, int r, int ly, int uy, int b, int n, int a, int lev) 
     : leftchild(l) 
     , rightchild(r) 
     , ly_index(ly) 
     , uy_index(uy) 
     , bit(b) 
     , nodepos(n) 
     , amount(a) 
     , level(lev) 
    {} 
}; 

i 3 funkcje.

static size_t counter1 = 0; 
static size_t counter2 = 0; 
static size_t counter3 = 0; 

int FindPoints(int leftchild, int rightchild, int ly_index, int uy_index, int bit, int nodepos, int amount, int level) 
{ 
    ++counter1; 
    leftchild = leftchild + (rightchild + ly_index + uy_index + bit + nodepos + amount + level)/100 - 1; 
    return leftchild ? FindPoints(leftchild, rightchild, ly_index, uy_index, bit, nodepos, amount, level) : 0; 
} 

int FindPointsRef(Args& a) 
{ 
    ++counter2; 
    a.leftchild = a.leftchild + (a.rightchild + a.ly_index + a.uy_index + a.bit + a.nodepos + a.amount + a.level)/100 - 1; 
    return a.leftchild ? FindPointsRef(a) : 0; 
} 

int FindPointsValue(Args a) 
{ 
    ++counter3; 
    a.leftchild = a.leftchild + (a.rightchild + a.ly_index + a.uy_index + a.bit + a.nodepos + a.amount + a.level)/100 - 1; 
    return a.leftchild ? FindPointsValue(a) : 0; 
} 

Wszyscy wykonują tę samą pracę, ale pierwszy bierze argumenty jak w swoim pytaniu, drugi bierze struct argumentów przez odniesienie a trzeci bierze struct przez wartość.

Zbudowałem program przy użyciu Visual Studio 2010, wypuszczono konfigurację x64 i zmierzyłem przy użyciu domowej klasy, która otacza funkcję Windows QueryPerformanceCounter i zapewnia wygodny operator wyjścia.

Główną funkcją wygląda następująco:

int main() 
{ 
    // define my timers 
    PersistentTimer timer_multi("multi args"); 
    PersistentTimer timer_ref("struct by reference"); 
    PersistentTimer timer_value("struct by value"); 

    int leftchild = 10000; // number of iterations; 10000 to prevent stack overflow 
    int rightchild = 1;  // sum of other values is < 100 (look to FindPoints* implementations) 
    int ly_index = 2; 
    int uy_index = 3; 
    int bit = 4; 
    int nodepos = 5; 
    int amount = 6; 
    int level = 7; 

    // define structs of arguments for second and third function 
    Args args_ref(leftchild, rightchild, ly_index, uy_index, bit, nodepos, amount, level); 
    Args args_copy(leftchild, rightchild, ly_index, uy_index, bit, nodepos, amount, level); 

    // return values initialized to a non zero value just to be sure that functions have done thir job 
    int a1 = 5; 
    timer_multi.measure([&]{ 
     a1 = FindPoints(leftchild, rightchild, ly_index, uy_index, bit, nodepos, amount, level); 
    }); 
    std::cout << "Multi result = " << a1 << "; multi iterations = " << counter1 << '\n'; 

    int a2 = 5; 
    timer_ref.measure([&]{ 
     a2 = FindPointsRef(args_ref); 
    }); 
    std::cout << "Ref result = " << a2 << "; ref iterations = " << counter2 << '\n'; 

    int a3 = 5; 
    timer_value.measure([&]{ 
     a3 = FindPointsValue(args_copy); 
    }); 
    std::cout << "Value result = " << a3 << "; value iterations = " << counter3 << '\n'; 

    // print timer results 
    std::cout << timer_multi << timer_ref << timer_value; 

    getchar(); 

} 
+0

Czy masz optymalizacje kompilatora? Ten kod nie działa dla mnie, daj mi przepełnienie stosu w FindPointRef, ponieważ wykonuje 54000 wywołań rekursywnych. Działa tylko wtedy, gdy aktywna jest optymalizacja kompilatora – amchacon

+0

@amchacon Tak, mam włączone optymalizacje kompilatora. Ale w VS2010 działa również w trybie debugowania. –

Powiązane problemy