2012-08-27 12 views
9

tylko krótka notatka, to nie jest praca domowa. Próbuję odświeżyć moje algorytmy. Mam zabawy z mergesort w C# i pisałem rekurencyjnej metody, które można sortować w oparciu o rodzajowych:C# sortować wydajność sortowania

class SortAlgorithms 
{ 

    public T[] MergeSort<T> (T[] unsortedArray) where T : System.IComparable<T> 
    { 
     T[] left, right; 
     int middle = unsortedArray.Length/2; 

     left = new T[middle]; 
     right = new T[unsortedArray.Length - middle]; 

     if (unsortedArray.Length <= 1) 
      return unsortedArray; 

     for (int i = 0; i < middle; i++) 
     { 
      left[i] = unsortedArray[i]; 
     } 

     for (int i = middle; i < unsortedArray.Length; i++) 
     { 
      right[i - middle] = unsortedArray[i]; 
     } 

     left = MergeSort(left); 

     right = MergeSort(right); 


     return Merge<T>(left, right); 
    } 

    private T[] Merge<T> (T[] left, T[] right) where T : System.IComparable<T> 
    { 
     T[] result = new T[left.Length + right.Length]; 

     int currentElement = 0; 

     while (left.Length > 0 || right.Length > 0) 
     { 
      if (left.Length > 0 && right.Length > 0) 
      { 
       if (left[0].CompareTo(right[0]) < 0) 
       { 
        result[currentElement] = left[0]; 
        left = left.Skip(1).ToArray(); 
        currentElement++; 
       } 
       else 
       { 
        result[currentElement] = right[0]; 
        right = right.Skip(1).ToArray(); 
        currentElement++; 
       } 
      } 
      else if (left.Length > 0) 
      { 
       result[currentElement] = left[0]; 
       left = left.Skip(1).ToArray(); 
       currentElement++; 
      } 
      else if (right.Length > 0) 
      { 
       result[currentElement] = right[0]; 
       right = right.Skip(1).ToArray(); 
       currentElement++; 
      } 
     } 

     return result; 
    } 
} 

To działa, ale to jest strasznie powolna. Użyłem System.Diagnostic.StopWatch do sprawdzenia wydajności w stosunku do Array.Sort (który używa algorytmu QuickSort) do porównania z moim MergeSort i różnica jest tak znacząca Zastanawiam się, czy może wprowadzam to źle. Wszelkie komentarze?

+1

Czy czytasz artykuł Jons? http://msmvps.com/blogs/jon_skeet/archive/2011/01/06/reimplementing-linq-to-objects-part-26c-optimizing-orderedenumerable.aspx –

+0

Czy wypróbowałeś tę samą implementację, ale bez generycznych? – pfries

+0

Świetne odpowiedzi chłopaki. Przepraszam, że trwało tak długo, aby odpowiedzieć, przepisałem kod i skończyło się na kodzie, który wygląda prawie dokładnie tak, jak sugerował Rafe. Ogromnie szybszy, ale wciąż znacznie wolniejszy niż rodzimy Array.Sort. Wciąż z tym grałem. – hobeau

Odpowiedz

12

Nie jestem programistą C#, ale czy problemem może być użycie takich instrukcji?

left = left.Skip(1).ToArray(); 

Może to zostać zaimplementowane w sposób wymuszający głęboką kopię podstawowej tablicy. Jeśli tak, spowodowałoby to spadek wydajności scalania z O (n) do O (n), natychmiastowo obniżając wydajność wynikowego sortowania scalającego z O (n log n) do O (n).

(Jest tak z powodu zmian nawrotów z

T = (1) O (1)

T (n) ≤ 2T (n/2) + O (n)

który ma rozwiązanie T (n) = O (n log n) do

T = (1) O (1)

T (n) ≤ 2T (n/2) + O (n)

który ma rozwiązanie T (n) = O (N)).

+1

Nie implementuje głębokiej kopii, ale powoduje utworzenie nowej tablicy (z płytką kopią). Nie wiem, jak zmienia ona granice, ale nadal nie jest idealny. –

+1

@ pst- Jeśli stworzy to nową tablicę i płytko skopiuje elementy, to również spowoduje spadek wydajności, ponieważ płytka kopia wymaga czasu O (n). – templatetypedef

+0

Dzięki za wyjaśnienia, nie byłem pewien, czy to był aspekt bycia głęboką kopią czy nie. –

2

Jesteś stale alokacji pamięci w postaci pośrednich tablic. Zastanów się nad ponownym użyciem oryginalnej tablicy.

1

Jako pozostałe dwie odpowiedzi powiedziały, że tworzysz nowe tablice w całym miejscu, poświęcając na to mnóstwo czasu i pamięci (prawdopodobnie zgaduję, że większość czasu i prawie wszystkie twoje wykorzystanie pamięci).

Na to jeszcze raz dodam, że wszystkie pozostałe rekordy są równe wolniejsze od iteracji, i używają więcej przestrzeni stosu (może nawet powodując przepełnienie z wystarczająco dużym problemem, w którym iteracja nie byłaby).

Jednak. Merge-sort nadaje się również do podejścia wielowątkowego, ponieważ możesz mieć różne wątki obsługujące różne części pierwszej partii partycjonowania.

Stąd gdyby ja grając z tego, moi Kolejne dwa eksperymenty byłyby:

  1. Do pierwszego bitu podziału, zamiast dzwonić MergeSort rekurencyjnie, chciałbym rozpocząć nowy wątek aż takie czas, w którym miałem wątek na rdzeń działający (czy powinienem to zrobić na fizyczny rdzeń lub wirtualny rdzeń w przypadku hiperwątkowości, sam jest czymś, z czym eksperymentowałem).
  2. Po wykonaniu tego, spróbuję ponownie napisać metodę rekursywną, aby zrobić to samo bez wywołań rekursywnych.

Po sprawa ToArray() została rozpatrzona, widząc, jak wielowątkowe podejście, że najpierw podzielić pracę wśród optymalnej liczby rdzeni, a następnie miał każdy rdzeń zrobić swoją pracę iteracyjnie, może być rzeczywiście bardzo interesujące.

+1

Zamiast zajmować się tym, czy spawnować nowy wątek, możesz po prostu utworzyć nowe "zadanie" i pozwolić mu przejść do puli wątków. Puli wątków będzie prawdopodobnie wielkość około liczby fizycznych procesorów. – Servy

+1

Z mojego doświadczenia wynika, że ​​sortowanie scalone (aczkolwiek na maszynie JVM w systemie Windows) "działa najlepiej" z około 1,5-2 * maks. * Wątkami na rdzeń w serii iCore 5/7 (nie należy lekceważyć kradzieży procesu ;-). Ponadto, wątki powinny * nie * być używane dla małych 'n' (gałęzi liści i krawędzi), a przełączanie się na sortowanie bez scalania dla krawędzi jest" wspólną optymalizacją ". –

+0

@pst" na rdzeń "jak w rdzeni lub hyperthreading wirtualnych rdzeni? –

0

Po pierwsze, oto link do usprawnione rozwiązania z podobnym pytaniem: Java mergesort, should the "merge" step be done with queues or arrays?

Twoje rozwiązanie jest powolny, ponieważ jesteś wielokrotnie przydzielania nowych subarrays. Alokacja pamięci jest znacznie droższa niż w przypadku większości innych operacji (masz koszt alokacji, koszt zbierania i utratę pamięci podręcznej). Zwykle nie jest to problemem, ale jeśli próbujesz zakodować ścisłą rutynę sortowania, liczy się to. W przypadku sortowania scalonego potrzebujesz tylko jednej tablicy docelowej i jednej tablicy tymczasowej.

Rozwinięcie nici do parallelise jest wciąż o rząd wielkości droższe. Więc nie rozwidlaj, chyba że masz ogromną ilość danych do posortowania.

Jak już wspomniałem w powyższej odpowiedzi, jednym ze sposobów przyspieszenia sortowania scalania jest wykorzystanie istniejącego porządku w tablicy wejściowej.