2010-02-13 6 views
12

Pracuję nad biblioteką równoległości dla języka programowania D. Teraz, gdy jestem całkiem zadowolony z podstawowych prymitywów (równoległe foreach, mapa, redukcja i zadania/przyszłość), zaczynam myśleć o kilku równoległych algorytmach wyższego poziomu. Wśród bardziej oczywistych kandydatów do równoległości jest sortowanie.(Kiedy) są praktyczne w zastosowaniach równoległych i jak można pisać wydajne?

Moje pierwsze pytanie to, czy są równoległe wersje algorytmów sortowania przydatnych w rzeczywistym świecie, czy są one w większości akademickie? Jeśli są przydatne, gdzie są przydatne? Osobiście rzadko używałbym ich w mojej pracy, po prostu dlatego, że zwykle kołkiem wszystkie moje rdzenie w 100% przy użyciu znacznie grubszy poziom równoległości niż pojedynczego wywołania sort().

Po drugie, wydaje się, że szybkie sortowanie jest niemal żenująco równoległe w przypadku dużych tablic, ale nie mogę uzyskać prawie liniowych przyspieszeń, które moim zdaniem powinny być osiągane. W przypadku szybkiego sortowania jedyną nieodłączną częścią jest pierwsza partycja. Próbowałem równolegle szybkiego sortowania po, po każdej partycji, równolegle sortować dwie podmary. W uproszczonej Pseudokod:

// I tweaked this number a bunch. Anything smaller than this and the 
// overhead is smaller than the parallelization gains. 
const smallestToParallelize = 500; 

void quickSort(T)(T[] array) { 
    if(array.length < someConstant) { 
     insertionSort(array); 
     return; 
    } 

    size_t pivotPosition = partition(array); 

    if(array.length >= smallestToParallelize) { 
     // Sort left subarray in a task pool thread. 
     auto myTask = taskPool.execute(quickSort(array[0..pivotPosition])); 
     quickSort(array[pivotPosition + 1..$]); 
     myTask.workWait(); 
    } else { 
     // Regular serial quick sort. 
     quickSort(array[0..pivotPosition]); 
     quickSort(array[pivotPosition + 1..$]); 
    } 
} 

Nawet dla bardzo dużych tablic, gdzie po raz pierwszy ze strefy jest znikoma, mogę tylko dostać około 30% SpeedUp na podwójnym rdzeniem, w porównaniu z czysto seryjnym wersji algorytmu . Zgaduję, że wąskim gardłem jest dostęp do pamięci dzielonej. Jakiekolwiek spostrzeżenia na temat tego, jak wyeliminować to wąskie gardło, czy jakie może być wąskie gardło?

Edytuj: Moja pula zadań ma ustaloną liczbę wątków, równą liczbie rdzeni w systemie minus 1 (ponieważ główny wątek również działa). Ponadto typ oczekiwania, którego używam, to oczekiwanie na pracę, tj. Jeśli zadanie jest uruchomione, ale nie zostało zakończone, wątek wywołujący workWait() kradnie inne zadania poza pulą i wykonuje je, dopóki nie zostanie zakończona operacja, na którą czeka. Jeśli zadanie nie zostanie uruchomione, zostanie zakończone w bieżącym wątku. Oznacza to, że oczekiwanie nie jest nieefektywne. Dopóki będzie praca do wykonania, wszystkie wątki będą zajęte.

+0

nie wiem jak zrobić quicksort parallelize lepiej, ale nie jest to wariant zwany samplesort który jest z natury znacznie szybciej niż quicksort waniliowym, io ile widzę, to powinien być w równym stopniu zrównoleglony. –

Odpowiedz

7

Należy pamiętać, że nie jestem ekspertem od równoległego rodzaju, a ludzie zrobić karierę naukową z równoległym rodzaju ale ...

1) są one użyteczne w rzeczywistym świecie.

oczywiście są, jeśli chcesz sortować coś drogiego (jak struny lub gorzej) i nie są ustalające wszystkie rdzenie.

  • myśleć kod UI gdzie trzeba uporządkować dużą dynamiczną listę ciągów na podstawie kontekstu
  • pomyśleć coś takiego jak Barnes-hut n-ciał sim gdzie trzeba sortować cząstki

2) Wygląda na to, że Quicksort przyspieszy liniowo, ale tak nie jest. Krok partycjonowania jest sekwencyjnym wąskim gardłem, zobaczysz to, jeśli będziesz profilować i będzie miał tendencję do zamykania się na poziomie 2-3x na czterordzeniowym rdzeniu.

Jeśli chcesz uzyskać dobre przyspieszenia na mniejszym systemie, musisz upewnić się, że koszty na zadanie są naprawdę małe i idealnie będziesz chciał zapewnić, że nie masz zbyt wielu wątków uruchomionych, tj. Niewiele więcej niż 2 na dwurdzeniowym. Pula wątków prawdopodobnie nie jest właściwą abstrakcją.

Jeśli chcesz uzyskać dobre przyspieszenia w większym systemie, musisz spojrzeć na równoległe sortowanie oparte na skanowaniu, są na ten temat dokumenty. sortowanie bitowe jest również dość łatwe do zrównania, podobnie jak sortowanie scalone. Równolegle sortowanie radix może być również przydatne, jest jeden w PPL (jeśli nie jesteś niechętny do Visual Studio 11).

3

nie jestem ekspertem ale ... tutaj jest to, co chciałbym spojrzeć na:

Przede wszystkim, słyszałem, że jako zasada, algorytmów, które wyglądają na małe kawałki problem od samego początku działa lepiej jako algorytmy równoległe.

Patrząc na swoją implementację, spróbuj ustawić przełącznik równoległy/szeregowy w drugą stronę: podziel tablicę i sortuj równolegle, aż uzyskasz N segmentów, a następnie przejdź do portu szeregowego. Jeśli mniej lub więcej chwytasz nowy wątek dla każdego równoległego przypadku, wtedy N powinno być ~ twoim rdzeniem. OTOH, jeśli twoja pula wątków ma ustalony rozmiar i działa jako kolejka krótkotrwałych delegatów, wtedy użyłbym N ~ 2 + krotności twojego rdzenia (tak, by rdzenie nie pozostały bezczynne, ponieważ jedna partycja skończyła się szybciej).

Inne szczypie:

  • pomiń myTask.wait(); na poziomie lokalnym i raczej mieć funkcję otoki, który czeka na wszystkich zadań.
  • Wykonaj osobną, szeregową implementację funkcji, która unika kontroli głębokości.
+0

+1. Ładne wyjaśnienie ... – bragboy

1

"Moje pierwsze pytanie dotyczy równoległych wersji algorytmów sortowania przydatnych w realnym świecie" - zależy to od rozmiaru zestawu danych, nad którym pracujesz w prawdziwej pracy. W przypadku małych zestawów danych odpowiedź brzmi "nie". W przypadku większych zestawów danych zależy to nie tylko od wielkości zbioru danych, ale także od specyficznej architektury systemu.

Jednym z czynników ograniczających, które zapobiegną oczekiwanemu wzrostowi wydajności, jest układ pamięci podręcznej systemu. Jeśli dane mieszczą się w pamięci podręcznej L1 rdzenia, to niewiele można zyskać przez sortowanie w wielu rdzeniach, ponieważ ponosisz karę za brak pamięci podręcznej L1 pomiędzy każdą iteracją algorytmu sortowania.

To samo rozumowanie dotyczy układów z wieloma pamięciami podręcznymi L2 i architekturami NUMA (niejednolitym dostępem do pamięci). Tak więc im więcej rdzeni zostanie rozesłanych przez sortowanie, należy odpowiednio zwiększyć stałą najmniejszej stałej równoległej.

Innym ograniczającym czynnikiem, który zidentyfikowałeś, jest dostęp do pamięci współużytkowanej lub rywalizacja przez magistralę pamięci. Ponieważ magistrala pamięci może spełnić tylko określoną liczbę dostępów do pamięci na sekundę; posiadanie dodatkowych rdzeni, które w zasadzie nic nie tylko czytają i zapisują w pamięci głównej, będą obciążać system pamięci.

Ostatnim czynnikiem, który powinienem wskazać, jest sama pula wątków, ponieważ może nie być tak wydajna, jak ci się wydaje. Ponieważ masz wątki, które kradną i generują pracę z udostępnianej kolejki, ta kolejka wymaga metod synchronizacji; i w zależności od tego, w jaki sposób są zaimplementowane, mogą powodować bardzo długie sekcje szeregowe w kodzie.

1

ja nie wiem, czy odpowiedzi są tu stosowane dłużej lub jeśli moje sugestie są stosowane do D.

Zresztą ...

Zakładając, że D to pozwala, zawsze istnieje możliwość dostarczania wskazówki wstępne do pamięci podręcznych. Rdzeń, o którym mowa, wymaga, aby dane, które wkrótce (a nie natychmiast) zostaną załadowane na pewien poziom pamięci podręcznej. W idealnym przypadku dane zostaną pobrane przed rozpoczęciem pracy nad rdzeniem. Bardziej prawdopodobne jest, że proces pobierania wstępnego będzie mniej więcej w drodze, co najmniej spowoduje mniej stanów oczekiwania, niż gdyby dane były "zimne."

Nadal będziesz ograniczany przez całkowitą przepustowość pamięci podręcznej do pamięci RAM, więc musisz zorganizować dane tak, aby tak wiele danych znajdowało się w wyłącznych pamięciach rdzenia rdzenia, że ​​może wydać sporą ilość czas potrzebny do zapisania zaktualizowanych danych:

Kod i dane muszą być uporządkowane zgodnie z koncepcją linii pamięci podręcznej (jednostek pobierania o długości 64 bajtów), która jest najmniejszą jednostką w pamięci podręcznej. w przypadku dwóch rdzeni praca musi być zorganizowana w taki sposób, aby system pamięci działał o połowę mniej na rdzeń (przy założeniu 100% skalowalności) tak jak poprzednio, gdy tylko jeden rdzeń działał, a praca nie była zorganizowana. tak wiele itd. To dość trudne, ale w żadnym wypadku niemożliwe, po prostu zależne ds jak wyobrażasz sobie, że jesteś w restrukturyzacji pracy. Jak zawsze istnieją rozwiązania, których nie można sobie wyobrazić ... dopóki ktoś tak nie zrobi!

Nie wiem, jak WYSIWYG D jest porównywany z C - z którego korzystam - ale generalnie myślę, że proces tworzenia skalowalnych aplikacji jest poprawiany przez to, jak programista może wpływać na kompilator w jego rzeczywistym generowaniu kodu maszynowego. W przypadku języków zinterpretowanych, praca tłumacza będzie wymagać tak dużo pamięci, że ryzykujesz, że nie będziesz w stanie odróżnić ulepszeń od ogólnego "szumu w tle".

Kiedyś napisałem wielowątkową skorupę, która przebiegała o 70% szybciej na dwóch rdzeniach w porównaniu do jednego i 100% na trzech rdzeniach w porównaniu z jednym. Cztery rdzenie działały wolniej niż trzy. Więc znam dylematy, przed którymi stoisz.

0

Chciałbym wskazać Państwu Sortowanie Zewnętrzne [1], które napotyka podobne problemy. Zwykle ta klasa algorytmów jest używana głównie do radzenia sobie z dużymi wolumenami danych, ale ich głównym punktem jest to, że dzielą one duże porcje na mniejsze i niepowiązane problemy, które są zatem naprawdę świetne do równoległego działania. Musisz "tylko" połączyć ze sobą częściowe wyniki, co nie jest tak proste (ale stosunkowo tanie w porównaniu do faktycznego sortowania).

Sortowanie z zewnętrznym scaleniem również działałoby naprawdę dobrze z nieznaną ilością wątków. Po prostu dzielenie obciążenia pracą jest dowolne i każdy fragment n elementów dodaje się do wątku za każdym razem, gdy jest jeden bezczynny, aż wszystkie jednostki robocze zostaną wykonane, po czym można rozpocząć dołączanie do nich.

[1] http://en.wikipedia.org/wiki/External_sorting

Powiązane problemy