sortowanie próbek

Samplesort to algorytm sortowania , który jest algorytmem dziel i zwyciężaj, często używanym w systemach przetwarzania równoległego. Konwencjonalne algorytmy sortowania typu „dziel i zwyciężaj” dzielą tablicę na podprzedziały lub segmenty. Wiadra są następnie sortowane pojedynczo, a następnie łączone razem. Jeśli jednak tablica jest rozłożona nierównomiernie, wydajność tych algorytmów sortowania może zostać znacznie ograniczona. Samplesort rozwiązuje ten problem, wybierając próbkę o rozmiarze s z n -sekwencja elementów i określenie zakresu przedziałów poprzez sortowanie próby i wybranie z wyniku p −1 < s elementów. Te elementy (zwane rozdzielaczami) następnie dzielą tablicę na p segmentów o mniej więcej równej wielkości. Sortowanie próbek jest opisane w artykule z 1970 r. „Samplesort: A Sampling Approach to Minimal Storage Tree Sorting”, autorstwa WD Frazer i AC McKellar.

Algorytm

Samplesort jest uogólnieniem sortowania szybkiego . Tam, gdzie quicksort dzieli swoje dane wejściowe na dwie części na każdym kroku, w oparciu o pojedynczą wartość zwaną przestawną, samplesort zamiast tego pobiera większą próbkę z danych wejściowych i odpowiednio dzieli dane na segmenty. Podobnie jak w przypadku sortowania szybkiego, rekurencyjnie sortuje zasobniki.

Aby opracować implementację sortowania próbek, należy określić liczbę segmentów p . Kiedy to nastąpi, rzeczywisty algorytm działa w trzech fazach:

  1. Próbka p −1 elementów z wejścia ( rozgałęźniki ). Sortuj te; każda para sąsiadujących rozdzielaczy definiuje następnie wiadro .
  2. Zapętl dane, umieszczając każdy element w odpowiednim zasobniku. (Może to oznaczać: wyślij go do procesora w wieloprocesorowym ).
  3. Posortuj każde z wiader.

Pełne posortowane dane wyjściowe to konkatenacja segmentów.

Powszechną strategią jest ustawienie p równe liczbie dostępnych procesorów. Dane są następnie rozdzielane między procesory, które wykonują sortowanie zasobników przy użyciu innego, sekwencyjnego algorytmu sortowania.

Pseudo kod

Poniższe zestawienie przedstawia wyżej wspomniany trzyetapowy algorytm jako pseudokod i pokazuje, jak algorytm działa w zasadzie. Poniżej A to nieposortowane dane, k to współczynnik nadpróbkowania, omówiony później, a p to liczba rozdzielaczy.

 0 function  sampleSort(A[1..n],  k  ,  p  ) // jeśli średni rozmiar zasobnika jest poniżej progu przełącz na np. sortowanie szybkie  if  n  /  k  < próg  then  smallSort(A) /* Krok 1 */ wybierz  S  = [  S  1  , ...,  S  (  p  −1)  k  ] losowo z // wybierz próbki posortuj  S  // posortuj próbkę [  s  ,  s  1  , ..., s  p  −1  ,   s  p  ] <- [-∞,  S  k  ,  S  2  k  , ...,  S  (  p  −1)  k  , ∞] // wybierz rozdzielacze /* Krok 2 */  dla każdego  a  w  A  znajdź  j  takie, że  s  j  −1  <  a  <=  s  j  umieść  a  w pojemniku  b  j  /* Krok 3 i konkatenacja */  powrót  concatenate(sampleSort(  b  1  ), ..., sampleSort(  b  k  )) 

Pseudokod różni się od oryginalnego algorytmu Frazera i McKellara. W pseudokodzie samplesort jest wywoływane rekurencyjnie. Frazer i McKellar wywołali samplesort tylko raz i użyli sortowania szybkiego we wszystkich kolejnych iteracjach.

Złożoność

Złożoność, podana w notacji Big O , dla równoległej implementacji z procesorami:

Znajdź rozdzielacze.

Wyślij do wiader.

do odczytu wszystkich węzłów
do nadawania
dla wyszukiwania binarnego dla wszystkich kluczy
, aby wysłać klucze do zasobnika

Sortuj wiadra.

gdzie jest złożoność podstawowej metody sortowania sekwencyjnego. Często .

Liczba porównań przeprowadzonych przez ten algorytm zbliża się do sekwencji W eksperymentach przeprowadzonych przez Frazera i McKellara algorytm wymagał o 15% mniej porównań niż sortowanie szybkie.

Próbkowanie danych

Dane mogą być próbkowane różnymi metodami. Niektóre metody obejmują:

  1. Wybierz równomiernie rozmieszczone próbki.
  2. Wybierz losowo wybrane próbki.

Nadpróbkowanie

Współczynnik nadpróbkowania określa, ile razy więcej elementów danych należy pobrać jako próbki przed określeniem rozdzielaczy. Celem jest uzyskanie dobrej reprezentacji rozkładu danych. Jeśli wartości danych są szeroko rozłożone, ponieważ nie ma wielu zduplikowanych wartości, wystarczy mały współczynnik próbkowania. W innych przypadkach, gdy w rozkładzie występuje wiele duplikatów, konieczny będzie większy współczynnik nadpróbkowania. W idealnym przypadku, po kroku 2, każde wiadro zawiera elementy. W tym przypadku sortowanie żadnego zasobnika nie zajmuje więcej czasu niż pozostałych, ponieważ wszystkie zasobniki są tej samej wielkości.

Po pobraniu liczby próbek niż to konieczne, próbki są sortowane. Następnie rozdzielaczami używanymi jako granice kubełków są próbki w pozycji sekwencja próbek (wraz z Displaystyle jako lewe i prawe granice odpowiednio dla segmentów najbardziej wysuniętych na lewo i najbardziej na prawo). Zapewnia to lepszą heurystykę dla dobrych rozdzielaczy niż .

Oszacowanie wielkości wiadra

Na podstawie otrzymanej wielkości próby można oszacować oczekiwaną wielkość koszyka, a zwłaszcza prawdopodobieństwo, że koszyk przekroczy określoną wielkość. Poniżej pokażemy, że dla współczynnika nadpróbkowania prawdopodobieństwo, że żadne wiadro nie ma więcej niż elementów jest większe niż .

Aby wejściem Aby procesor mógł uzyskać więcej elementów niż , musi istnieć podsekwencja wprowadzania długości pobiera się maksymalnie S próbek. prawdopodobieństwo . Można to przedstawić jako zmienną losową:

Dla oczekiwanej wartości posiada: X ja { \ displaystyle X_ {i}}

Zostanie to wykorzystane do oszacowania :

Korzystając teraz z ograniczenia Chernoffa , można pokazać:

Wiele identycznych kluczy

W przypadku wielu identycznych kluczy algorytm przechodzi przez wiele poziomów rekurencji, na których sekwencje są sortowane, ponieważ cała sekwencja składa się z identycznych kluczy. Można temu przeciwdziałać, wprowadzając koszyki równości. Elementy równe osi obrotu są sortowane do odpowiedniego zasobnika równości, który można zaimplementować tylko z jedną dodatkową gałęzią warunkową. Zasobniki równości nie są dalej sortowane. więcej niż prawdopodobnie staną się

Zastosowania w systemach równoległych

Przykład równoległego sortowania próbek na { =

Sortowanie próbek jest często używane w systemach równoległych, w tym w systemach rozproszonych, takich jak masowe synchroniczne maszyny równoległe . Ze względu na zmienną liczbę rozdzielaczy (w przeciwieństwie do tylko jednej osi w Quicksort ), Samplesort jest bardzo dobrze przystosowany i intuicyjny do równoległości i skalowania. Ponadto Samplesort jest również bardziej wydajny pod względem pamięci podręcznej niż implementacje np. sortowania szybkiego.

Równoległość jest realizowana przez podzielenie sortowania dla każdego procesora lub węzła, gdzie liczba zasobników jest równa . Sortowanie próbek jest wydajne w systemach równoległych, ponieważ każdy procesor otrzymuje w przybliżeniu ten sam rozmiar zasobnika . Ponieważ zasobniki są sortowane jednocześnie, procesory zakończą sortowanie mniej więcej w tym samym czasie, dzięki czemu procesor nie będzie czekał na inne.

W systemach rozproszonych rozdzielacze są wybierane przez pobranie elementów na każdym procesorze, sortowanie wynikowych za pomocą algorytmu sortowania rozproszonego, pobranie każdego elementu i k rozgłaszanie wyniku do wszystkich procesorów. To kosztuje do sortowania elementy także p _ procesory.

Dzięki wynikowym rozdzielaczom każdy procesor umieszcza własne dane wejściowe w lokalnych zasobnikach. To zajmuje z wyszukiwaniem binarnym . Następnie lokalne zasobniki są redystrybuowane do procesorów. Procesor pobiera lokalne zasobniki procesorów i sortuje Dystrybucja przyjmuje czas, gdzie to rozmiar największego wiadra. Sortowanie lokalne trwa .

Eksperymenty przeprowadzone na początku lat 90. na superkomputerach Connection Machine wykazały, że samplesort jest szczególnie dobry w sortowaniu dużych zestawów danych na tych maszynach, ponieważ wiąże się z niewielkim obciążeniem komunikacyjnym między procesorami. W najnowszych procesorach graficznych algorytm może być mniej skuteczny niż jego alternatywy. [ potrzebne źródło ]

Wydajna implementacja Samplesort

Animowany przykład Super Skalarnego Sortowania Próbek. W każdym kroku porównywane liczby są oznaczane na niebiesko, a liczby, które w inny sposób są odczytywane lub zapisywane, są oznaczane na czerwono.

Jak opisano powyżej, algorytm sortowania próbek rozdziela elementy zgodnie z wybranymi rozdzielaczami. Efektywną strategię implementacji zaproponowano w artykule „Super Skalarne Sortowanie Próbek”. Implementacja zaproponowana w artykule wykorzystuje dwie tablice o rozmiarze tablica zawierająca dane wejściowe i tablica tymczasowa) do wydajnej implementacji. W związku z tym ta wersja implementacji nie jest algorytmem w miejscu.

W każdym kroku rekurencji dane są kopiowane do innej tablicy w sposób podzielony na partycje. Jeśli dane znajdują się w tablicy tymczasowej w ostatnim kroku rekurencji, dane są kopiowane z powrotem do oryginalnej tablicy.

Określanie kubełków

W algorytmie sortowania opartym na porównaniach operacja porównania jest częścią o największym znaczeniu dla wydajności. W Samplesort odpowiada to określeniu segmentu dla każdego elementu. To wymaga elementu

Superskalarne sortowanie próbek wykorzystuje zrównoważone drzewo wyszukiwania, które jest niejawnie przechowywane w tablicy t . Korzeń jest przechowywany w 0, lewy następnik jest przechowywany w a prawy następnik jest przechowywany w . Biorąc pod uwagę drzewo wyszukiwania t , algorytm oblicza liczbę wiadra j elementu w następujący sposób (zakładając, że ma wartość 1, jeśli jest to prawda , a 0 w przeciwnym razie):

 j  := 1 powtórz logarytm  2  (  p  ) razy  j  := 2  jot  + (  a  >  t  jot  )  j  :=  j  -  p  + 1 

Ponieważ liczba zasobników k jest znana w czasie kompilacji, kompilator może rozwinąć tę pętlę. Operacja porównania jest realizowana za pomocą predykowanych instrukcji . Dzięki temu nie występują błędne przewidywania gałęzi , które znacząco spowalniałyby operację porównywania.

Partycjonowanie

W celu wydajnego podziału elementów algorytm musi z góry znać rozmiary zasobników. Aby podzielić elementy sekwencji i umieścić je w tablicy, musimy wcześniej znać rozmiar wiader. Naiwny algorytm mógłby policzyć liczbę elementów każdego segmentu. Następnie elementy można było wstawić do drugiej tablicy we właściwym miejscu. Korzystając z tego, należy dwukrotnie określić kubełek dla każdego elementu (jeden raz, aby policzyć elementy w kubełku, a drugi raz, aby je wstawić).

Aby uniknąć tego podwojenia porównań, superskalarne sortowanie próbek wykorzystuje dodatkową tablicę ), która przypisuje każdy indeks elementów do wiadra. Najpierw algorytm określa zawartość, a następnie umieszcza elementy w wiadrze określonym przez . Tablica wiąże się również z kosztami miejsca do przechowywania, ale ponieważ musi tylko przechowywać bitów, koszt ten jest niewielki w porównaniu z przestrzenią tablicy wejściowej.

Sortowanie próbek na miejscu

Kluczową wadą wydajnej implementacji Samplesort pokazanej powyżej jest to, że nie jest ona na miejscu i wymaga drugiej tymczasowej tablicy o takim samym rozmiarze jak sekwencja wejściowa podczas sortowania. Wydajne implementacje np. szybkiego sortowania są na miejscu, dzięki czemu zajmują mniej miejsca. Jednak Samplesort można również zaimplementować w miejscu.

Algorytm w miejscu jest podzielony na cztery fazy:

  1. Próbkowanie , które jest równoważne próbkowaniu w wyżej wymienionej wydajnej implementacji.
  2. Klasyfikacja lokalna na każdym procesorze, która grupuje dane wejściowe w bloki w taki sposób, że wszystkie elementy w każdym bloku należą do tego samego zasobnika, ale zasobniki niekoniecznie są ciągłe w pamięci.
  3. Permutacja bloków ustawia bloki w globalnie poprawnej kolejności.
  4. Czyszczenie przesuwa niektóre elementy na krawędziach wiader.

Oczywistą wadą tego algorytmu jest to, że odczytuje i zapisuje każdy element dwukrotnie, raz w fazie klasyfikacji i raz w fazie permutacji bloków. Jednak algorytm działa do trzech razy szybciej niż inni najnowocześniejsi konkurenci działający na miejscu i do 1,5 razy szybciej niż inni najnowocześniejsi konkurenci sekwencyjni. Ponieważ próbkowanie zostało już omówione powyżej, trzy późniejsze etapy zostaną bardziej szczegółowo omówione poniżej.

Klasyfikacja lokalna

W pierwszym kroku tablica wejściowa jest dzielona na bloków o równej wielkości, po jednym dla każdego procesora Każdy procesor dodatkowo przydziela bufory o takim samym rozmiarze jak bloki, po jednym dla każdego zasobnika. Następnie każdy procesor skanuje swój pasek i przenosi elementy do bufora odpowiedniego zasobnika. Jeśli bufor jest pełny, bufor jest zapisywany na pasku procesorów, zaczynając od przodu. Zawsze jest co najmniej jeden rozmiar bufora pustej pamięci, ponieważ aby bufor został zapisany (tzn. bufor jest pełny), trzeba było przeskanować co najmniej cały rozmiar bufora zawierający więcej elementów niż elementy zapisane z powrotem. Zatem każdy pełny blok zawiera elementy tego samego segmentu. Podczas skanowania śledzony jest rozmiar każdego wiadra.

Zablokuj permutację

Najpierw wykonywana jest operacja sumy przedrostków, która oblicza granice przedziałów. Ponieważ jednak w tej fazie przesuwane są tylko pełne bloki, granice są zaokrąglane w górę do wielokrotności rozmiaru bloku i przydzielany jest pojedynczy bufor przepełnienia. Przed rozpoczęciem permutacji bloków niektóre puste bloki mogą wymagać przesunięcia na koniec wiadra. wskaźnik zapisu ustawiany na początek podtablicy wiadra każdego wiadra i wskaźnik odczytu jest ustawiony na ostatni niepusty blok w podtablicy zasobnika dla każdego zasobnika.

pracę, każdemu procesorowi przypisuje się inny podstawowy zasobnik blok. W każdym kroku, jeśli oba bufory wymiany są puste, procesor zmniejsza wskaźnik odczytu blok w i umieszcza go w jednym ze swoich buforów wymiany. Po określeniu docelowego bloku poprzez sklasyfikowanie pierwszego elementu bloku, zwiększa on wskaźnik zapisu, odczytuje b re blok blok w docelowym Jeśli bufory wymiany są znowu puste. W przeciwnym razie blok pozostający w buforach wymiany musi zostać wstawiony do docelowego zasobnika.

Jeśli wszystkie bloki w podtablicy głównego zasobnika procesora znajdują się we właściwym zasobniku, następny zasobnik jest wybierany jako zasobnik podstawowy. Jeśli procesor raz wybierze wszystkie zasobniki jako zasobnik podstawowy, procesor jest zakończony.

Posprzątać

Ponieważ w fazie permutacji bloków przesunięto tylko całe bloki, niektóre elementy mogą nadal być nieprawidłowo rozmieszczone wokół granic segmentów. Ponieważ w tablicy musi być wystarczająco dużo miejsca dla każdego elementu, te nieprawidłowo umieszczone elementy można przenieść do pustych miejsc od lewej do prawej, na koniec uwzględniając bufor przepełnienia.

Zobacz też

Linki zewnętrzne

Sortowanie próbek i pochodne Frazera i McKellara:

Przystosowany do użytku na komputerach równoległych: