Biorąc pod uwagę zainteresowanie tym pytaniem , pomyślałem, że interesujące byłoby uczynienie odpowiedzi nieco bardziej obiektywnymi i ilościowymi poprzez zaproponowanie konkursu.
Pomysł jest prosty: wygenerowałem plik binarny zawierający 50 milionów podwójnych rozkładów gaussowskich (średnia: 0, stdev 1). Celem jest stworzenie programu, który posortuje je w pamięci tak szybko, jak to możliwe. Bardzo prosta implementacja referencji w Pythonie zajmuje 1m4s. Jak nisko możemy zejść?
Reguły są następujące: odpowiedz za pomocą programu, który otworzy plik „gaussian.dat” i posortuje liczby w pamięci (nie trzeba ich wysyłać), oraz instrukcje dotyczące budowania i uruchamiania programu. Program musi działać na moim komputerze Arch Linux (co oznacza, że możesz używać dowolnego języka programowania lub biblioteki, które można łatwo zainstalować w tym systemie).
Program musi być wystarczająco czytelny, aby upewnić się, że można go bezpiecznie uruchomić (proszę, nie ma rozwiązania tylko dla asemblera!).
Odpowiedzi uruchomię na moim komputerze (czterordzeniowy, 4 gigabajty pamięci RAM). Najszybsze rozwiązanie otrzyma zaakceptowaną odpowiedź i nagrodę za 100 punktów :)
Program używany do generowania liczb:
#!/usr/bin/env python
import random
from array import array
from sys import argv
count=int(argv[1])
a=array('d',(random.gauss(0,1) for x in xrange(count)))
f=open("gaussian.dat","wb")
a.tofile(f)
Prosta implementacja referencyjna:
#!/usr/bin/env python
from array import array
from sys import argv
count=int(argv[1])
a=array('d')
a.fromfile(open("gaussian.dat"),count)
print "sorting..."
b=sorted(a)
EDYCJA: tylko 4 GB pamięci RAM, przepraszam
EDYCJA 2: Zauważ, że celem konkursu jest sprawdzenie, czy możemy wykorzystać wcześniejsze informacje o danych . to nie powinno być pasujące dopasowanie między różnymi implementacjami języka programowania!
źródło
Odpowiedzi:
Oto rozwiązanie w C ++, które najpierw dzieli liczby na segmenty z taką samą oczekiwaną liczbą elementów, a następnie sortuje każdy segment osobno. Wstępnie oblicza tabelę funkcji skumulowanego rozkładu w oparciu o niektóre formuły z Wikipedii, a następnie interpoluje wartości z tej tabeli, aby uzyskać szybkie przybliżenie.
Kilka kroków przebiega w wielu wątkach, aby wykorzystać cztery rdzenie.
Aby go skompilować i uruchomić, użyj tego polecenia:
EDYCJA: Wszystkie segmenty są teraz umieszczane w tej samej tablicy, aby wyeliminować potrzebę kopiowania segmentów z powrotem do tablicy. Zmniejszono również rozmiar tabeli z wartościami wstępnie obliczonymi, ponieważ wartości są wystarczająco dokładne. Jeśli jednak zmienię liczbę segmentów powyżej 256, uruchomienie programu potrwa dłużej niż przy tej liczbie segmentów.
EDYCJA: Ten sam algorytm, inny język programowania. Użyłem C ++ zamiast Javy, a czas działania skrócił się z ~ 3.2s do ~ 2.35s na moim komputerze. Optymalna liczba segmentów nadal wynosi około 256 (znowu na moim komputerze).
Nawiasem mówiąc, TBB jest naprawdę niesamowity.
EDYCJA: Zainspirowało mnie świetne rozwiązanie Alexandru i zastąpiłem std :: sort w ostatniej fazie zmodyfikowaną wersją jego sortowania radix. Użyłem innej metody, aby radzić sobie z liczbami dodatnimi / ujemnymi, mimo że potrzebuje ona więcej przejść przez tablicę. Postanowiłem również dokładnie posortować tablicę i usunąć sortowanie wstawiania. Później poświęcę trochę czasu na sprawdzenie, w jaki sposób te zmiany wpływają na wydajność i ewentualnie je cofają. Jednak zastosowanie sortowania radix skróciło czas z ~ 2,35 s do ~ 1,63 s.
źródło
Nie będąc inteligentnym, aby zapewnić znacznie szybszy naiwny sorter, oto jeden w C, który powinien być prawie równoważny z twoim Pythonem:
Skompilowane
gcc -O3
, na moim komputerze zajmuje to ponad minutę mniej niż Python: około 11 s w porównaniu do 87 s.źródło
Podzieliłem na segmenty w oparciu o odchylenie standardowe, które najlepiej podzielić na 4. Edycja: Przepisany na partycję na podstawie wartości x w http://en.wikipedia.org/wiki/Error_function#Table_of_values
http://www.wolframalpha.com/input/?i=percentages+by++normal+distribution
Próbowałem użyć mniejszych segmentów, ale wydawało się, że raz * 2 miało to niewielki wpływ na liczbę dostępnych rdzeni. Bez równoległych kolekcji zajęłoby 37 sekund na moim pudełku i 24 z równoległymi kolekcjami. Jeśli partycjonowanie odbywa się za pomocą dystrybucji, nie możesz po prostu użyć tablicy, więc jest więcej narzutu. Nie jestem pewien, kiedy wartość będzie pudełkowana / rozpakowywana w scala.
Używam scala 2.9 do zbierania równoległego. Możesz po prostu pobrać jego dystrybucję tar.gz.
Aby skompilować: scalac SortFile.scala (właśnie skopiowałem go bezpośrednio do folderu scala / bin.
Aby uruchomić: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (uruchomiłem go z 2 koncertami pamięci RAM i dostałem mniej więcej w tym samym czasie)
Edycja: Usunięto przydzielanie bezpośrednie, wolniejsze niż tylko przydzielanie. Usunięto zalewanie początkowego rozmiaru buforów tablic. Właściwie odczytał wszystkie wartości 50000000. Przepisano, aby uniknąć problemów z autoboxingiem (wciąż wolniej niż naiwny c)
źródło
Po prostu włóż to do pliku cs i skompiluj teoretycznie z csc: (Wymaga mono)
źródło
Ponieważ wiesz, co to jest dystrybucja, możesz użyć sortowania bezpośredniego O (N). (Jeśli zastanawiasz się, co to jest, załóżmy, że masz talię 52 kart i chcesz ją posortować. Po prostu 52 pojemniki i wrzuć każdą kartę do osobnego pojemnika.)
Masz 5e7 podwójnych. Przydziel tablicę wyników R liczby 5e7 podwójnej. Weź każdą liczbę
x
i zdobądźi = phi(x) * 5e7
. Zasadniczo takR[i] = x
. Mają sposób radzenia sobie z kolizjami, na przykład przenoszenie numeru, z którym może kolidować (jak w prostym kodowaniu mieszającym). Alternatywnie możesz zwiększyć R kilka razy, wypełniając go unikalną pustą wartością. Na koniec po prostu zamiatasz elementy R.phi
jest po prostu funkcją rozkładu skumulowanego gaussa. Konwertuje liczbę rozproszoną gaussa między +/- nieskończonością na jednolitą liczbę rozproszoną między 0 a 1. Prostym sposobem jej obliczenia jest wyszukiwanie i interpolacja tabeli.źródło
Oto kolejne sekwencyjne rozwiązanie:
Wątpię, czy pobije to rozwiązanie wielowątkowe, ale czasy na moim laptopie i7 są (stdsort to rozwiązanie C ++ podane w innej odpowiedzi):
Zauważ, że to rozwiązanie ma liniową złożoność czasową (ponieważ wykorzystuje specjalną reprezentację podwójnych).
EDYCJA : Naprawiono wzrost kolejności elementów.
EDYCJA : Poprawiona prędkość o prawie pół sekundy.
EDYCJA : Poprawiona prędkość o kolejne 0,7 sekundy. Sprawiono, że algorytm jest bardziej przyjazny dla pamięci podręcznej.
EDYCJA : Poprawiona prędkość o kolejne 1 sekundę. Ponieważ jest tam tylko 50 000 000 elementów, mogę częściowo posortować mantysę i użyć sortowania wstawek (co jest przyjazne dla pamięci podręcznej), aby naprawić elementy nie na miejscu. Ten pomysł usuwa około dwóch iteracji z ostatniej pętli sortowania podstawników.
EDYCJA : 0,16 sekundy mniej. Pierwszy std :: reverse można wyeliminować, jeśli kolejność sortowania zostanie odwrócona.
źródło
Biorąc rozwiązanie Christiana Ammera i równolegle go z gwintowanymi elementami konstrukcyjnymi Intela
Jeśli masz dostęp do biblioteki Intel Performance Primitive (IPP), możesz użyć jej sortowania radix. Po prostu wymień
z
i
z
Na moim dwurdzeniowym laptopie czasy są
źródło
Co powiesz na implementację równoległego szybkiego sortowania, który wybiera swoje wartości przestawne na podstawie statystyk rozkładu, zapewniając w ten sposób równe rozmiary partycji? Pierwszy element przestawny miałby wartość średnią (w tym przypadku zero), kolejna para byłaby na 25. i 75. percentylu (+/- -0.67449 odchylenia standardowe) i tak dalej, przy każdej partycji o połowę pozostały zestaw danych więcej lub mniej idealnie.
źródło
Bardzo brzydkie (po co używać tablic, kiedy mogę używać zmiennych kończących się cyframi), ale szybki kod (moja pierwsza próba std :: Thread), cały czas (czas rzeczywisty) w moim systemie 1,8 s (w porównaniu do std :: sort () 4,8 s), skompiluj z g ++ -std = c ++ 0x -O3 -march = natywny -pthread Wystarczy przekazać dane przez stdin (działa tylko dla 50M).
// Edycja została zmieniona, aby odczytać plik gaussian.dat.
źródło
Wykorzystanie rozwiązania C ++
std::sort
(ostatecznie szybsze niż qsort, w odniesieniu do wydajności qsort vs std :: sort )Nie mogę rzetelnie powiedzieć, ile to trwa, ponieważ mam tylko 1 GB na moim komputerze, a dzięki podanemu kodowi Python mogłem stworzyć
gaussian.dat
plik tylko z 25 mln kopii podwójnych (bez błędu pamięci). Ale jestem bardzo zainteresowany, jak długo działa algorytm std :: sort.źródło
sort.h
pliku, aby skompilować go w C ++. Było około dwa razy wolniej niżstd::sort
. Nie wiem dlaczego, może z powodu optymalizacji kompilatora?Oto mieszanka radixu Alexandru z inteligentnym obrotowym gwintowaniem Zjareka. Skompiluj to z
Możesz zmienić rozmiar podstawy określając STEP (np. Dodaj -DSTEP = 11). Uważam, że najlepszy dla mojego laptopa jest 8 (domyślnie).
Domyślnie dzieli problem na 4 części i uruchamia go na wielu wątkach. Możesz to zmienić, przekazując parametr głębokości do wiersza poleceń. Więc jeśli masz dwa rdzenie, uruchom go jako
a jeśli masz 16 rdzeni
Maksymalna głębokość w tej chwili wynosi 6 (64 wątków). Jeśli ustawisz zbyt wiele poziomów, po prostu spowolnisz kod.
Próbowałem też sortowania radix z biblioteki Intel Performance Primitive (IPP). Implementacja Alexandru mocno niepokoi IPP, przy czym IPP jest o około 30% wolniejszy. Ta odmiana jest tu również uwzględniona (skomentowana).
EDYCJA : Wdrożyłem ulepszenia pamięci podręcznej Alexandru, co skróciło około 30% czasu na moim komputerze.
EDYCJA : To implementuje sortowanie rekurencyjne, więc powinno dobrze działać na 16-rdzeniowej maszynie Alexandru. Wykorzystuje także ostatnie ulepszenie Alexandru i usuwa jeden z rewersów. Dla mnie to dało 20% poprawę.
EDYCJA : Naprawiono błąd znaku, który powodował nieefektywność, gdy jest więcej niż 2 rdzenie.
EDYCJA : Usunięto lambda, aby skompilowała się ze starszymi wersjami gcc. Obejmuje skomentowaną odmianę kodu IPP. Poprawiłem również dokumentację dotyczącą działania na 16 rdzeniach. O ile wiem, jest to najszybsza implementacja.
EDYCJA : Naprawiono błąd, gdy STEP nie jest 8. Zwiększono maksymalną liczbę wątków do 64. Dodano kilka informacji o taktowaniu.
źródło
step
(11 było optymalne na moim laptopie).int cnt[mask]
powinien byćint cnt[mask + 1]
. Aby uzyskać lepsze wyniki, użyj stałej wartościint cnt[1 << 16]
.Myślę, że to naprawdę zależy od tego, co chcesz zrobić. Jeśli chcesz posortować grupę Gaussów, to ci to nie pomoże. Ale jeśli chcesz garść posortowanych Gaussów, tak będzie. Nawet jeśli to trochę pomija ten problem, myślę, że interesujące byłoby porównanie z faktycznymi procedurami sortowania.
Jeśli chcesz być szybki, zrób mniej.
Zamiast generować wiązkę losowych próbek z rozkładu normalnego, a następnie sortować, można wygenerować wiązkę próbek z rozkładu normalnego w uporządkowanej kolejności.
Możesz użyć rozwiązania tutaj, aby wygenerować n jednolitych liczb losowych w posortowanej kolejności. Następnie możesz użyć odwrotnego cdf (scipy.stats.norm.ppf) rozkładu normalnego, aby przekształcić jednolite liczby losowe w liczby z rozkładu normalnego za pomocą próbkowania z transformacją odwrotną .
Jeśli chcesz zabrudzić sobie ręce, domyślam się, że możesz przyspieszyć wiele odwrotnych obliczeń cdf, stosując jakąś metodę iteracyjną i wykorzystując poprzedni wynik jako początkowe przypuszczenie. Ponieważ domysły będą bardzo bliskie, prawdopodobnie jedna iteracja zapewni dużą dokładność.
źródło
Wypróbuj to zmieniające się rozwiązanie Guvante z tym Main (), zaczyna sortować, gdy tylko odczyt 1/4 IO zostanie zakończony, w moim teście jest szybszy:
źródło
Ponieważ znasz rozkład, moim pomysłem byłoby zrobienie k segmentów, każdy z taką samą oczekiwaną liczbą elementów (ponieważ znasz rozkład, możesz to obliczyć). Następnie w czasie O (n) zmieść tablicę i włożyć elementy do ich wiader.
Następnie równolegle posortuj wiadra. Załóżmy, że masz k wiader i n elementów. Sortowanie zajmie (n / k) lg (n / k) czas. Załóżmy teraz, że masz procesory p, których możesz użyć. Ponieważ wiadra można sortować niezależnie, masz do czynienia z mnożnikiem pułapu (k / p). Daje to końcowy czas działania n + ceil (k / p) * (n / k) lg (n / k), co powinno być o wiele szybsze niż n lg n, jeśli dobrze wybierzesz k.
źródło
std::sort()
, ale jest znacznie wolniejsze niż rozwiązanie radixsort Alexandru.Jednym z pomysłów optymalizacji niskiego poziomu jest dopasowanie dwóch podwójnych danych do rejestru SSE, aby każdy wątek działał jednocześnie z dwoma elementami. W przypadku niektórych algorytmów może to być skomplikowane.
Inną rzeczą do zrobienia jest posortowanie tablicy w części przyjazne dla pamięci podręcznej, a następnie scalenie wyników. Należy zastosować dwa poziomy: na przykład pierwsze 4 KB dla L1, a następnie 64 KB dla L2.
Powinno to być bardzo przyjazne dla pamięci podręcznej, ponieważ sortowanie kubełkowe nie wyjdzie poza pamięć podręczną, a końcowe scalanie będzie przechodzić pamięć sekwencyjnie.
Obecnie obliczenia są znacznie tańsze niż dostęp do pamięci. Mamy jednak dużą liczbę elementów, więc trudno powiedzieć, jaki jest rozmiar tablicy, gdy głupie sortowanie z pamięcią podręczną jest wolniejsze niż wersja o niskiej złożoności, która nie obsługuje pamięci podręcznej.
Ale nie przedstawię implementacji powyższego, ponieważ zrobiłbym to w systemie Windows (VC ++).
źródło
Oto implementacja sortowania kubełkowego skanowania liniowego. Myślę, że jest szybszy niż wszystkie obecne implementacje jednowątkowe oprócz sortowania radix. Powinien mieć liniowy oczekiwany czas działania, jeśli odpowiednio oceniam plik cdf (używam interpolacji liniowej wartości znalezionych w Internecie) i nie popełniłem żadnych błędów, które spowodowałyby nadmierne skanowanie:
źródło
Nie wiem, dlaczego nie mogę edytować mojego poprzedniego postu, więc oto nowa wersja, 0,2 sekundy szybciej (ale około 1,5 s szybciej w czasie procesora (użytkownika)). To rozwiązanie ma 2 programy, najpierw wstępnie oblicza kwantyle dla rozkładu normalnego do sortowania w segmentach i zapisuje je w tabeli, t [double * scale] = indeks segmentu, gdzie skala jest dowolną liczbą, która umożliwia rzutowanie do dwukrotności. Następnie program główny może wykorzystać te dane do umieszczenia podwójnej liczby w poprawnym segmencie. Ma jedną wadę, jeśli dane nie są gaussowskie, nie będzie działać poprawnie (a także istnieje prawie zerowa szansa na niepoprawną pracę dla normalnej dystrybucji), ale modyfikacja dla specjalnego przypadku jest łatwa i szybka (tylko liczba kontroli segmentów i spada do standardowej ::sortować()).
Kompilacja: g ++ => http://pastebin.com/WG7pZEzH program pomocniczy
g ++ -std = c ++ 0x -O3 -march = natywny -pthread => http://pastebin.com/T3yzViZP główny program sortujący
źródło
Oto kolejne rozwiązanie sekwencyjne. Ten wykorzystuje fakt, że elementy są normalnie rozmieszczone, i myślę, że pomysł ma ogólne zastosowanie, aby uzyskać sortowanie zbliżone do czasu liniowego.
Algorytm wygląda następująco:
phi()
funkcja w implementacji)size * phi(x)
Niestety, ukryta stała jest dość duża, a to rozwiązanie jest dwa razy wolniejsze niż algorytm sortowania radix.
źródło
Mój osobisty faworyt wykorzystujący wątkowe bloki konstrukcyjne Intela został już opublikowany, ale oto prymitywne równoległe rozwiązanie z użyciem JDK 7 i jego nowego interfejsu API rozwidlenia / dołączania:
Ważna informacja : wziąłem adaptację szybkiego sortowania dla fork / join z: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
Aby to uruchomić, potrzebujesz wersji beta JDK 7 (http://jdk7.java.net/download.html).
Na moim 2,93 GHz Quad Core i7 (OS X):
Odwołanie do Pythona
Widelec / łączenie Java JDK 7
Próbowałem też trochę eksperymentować z równoległym czytaniem i konwertowaniem bajtów na podwójne, ale nie zauważyłem żadnej różnicy.
Aktualizacja:
Jeśli ktoś chce eksperymentować z równoległym ładowaniem danych, wersja ładowania równoległego jest poniżej. Teoretycznie może to jeszcze trochę przyspieszyć, jeśli twoje urządzenie IO ma wystarczającą pojemność równoległą (zwykle dyski SSD). Tworzenie dublów z bajtów wiąże się również z pewnym nakładem, więc potencjalnie może to również przyspieszyć równolegle. Na moich systemach (Ubuntu 10.10 / Nehalem Quad / Intel X25M SSD i OS X 10.6 / i7 Quad / Samsung SSD) nie widziałem żadnej prawdziwej różnicy.
Aktualizacja 2:
Wykonałem kod na jednej z naszych 12 podstawowych maszyn programistycznych z niewielką modyfikacją, aby ustawić stałą liczbę rdzeni. To dało następujące wyniki:
W tym systemie wypróbowałem także wersję Pythona, która zajęła 1m2.994s oraz wersję C ++ Zjareka, która zajęła 1.925s (z jakiegoś powodu wersja C ++ Zjareka wydaje się działać stosunkowo szybciej na komputerze static_rtti).
Próbowałem również, co się stanie, jeśli podwoję rozmiar pliku do 100 000 000 kopii dwukrotnie:
W tym przypadku wersja C ++ Zjarek zajęła 3.968s. Python trwał tu zbyt długo.
150 000 000 podwójnych:
W tym przypadku wersja C ++ w Zjarek miała 6.044s. Nawet nie próbowałem Pythona.
Wersja C ++ jest bardzo spójna z wynikami, w których Java trochę się waha. Najpierw staje się trochę bardziej wydajny, gdy problem staje się większy, ale potem znów mniej wydajny.
źródło
Wersja wykorzystująca tradycyjne wątki. Kod scalania skopiowany z odpowiedzi Guvante. Kompiluj z
g++ -O3 -pthread
.Na moim laptopie otrzymuję następujące wyniki:
źródło
Oto sekwencyjna implementacja C99, która próbuje naprawdę wykorzystać znaną dystrybucję. Zasadniczo wykonuje pojedynczą rundę sortowania segmentu z wykorzystaniem informacji o dystrybucji, a następnie kilka rund szybkiego sortowania w każdym segmencie, zakładając jednolity rozkład w granicach segmentu, a na koniec zmodyfikowany sortowanie selekcji w celu skopiowania danych z powrotem do pierwotnego bufora. Quicksort zapamiętuje punkty podziału, więc sortowanie selekcji musi działać tylko na małych porcjach. I pomimo (bo?) Całej tej złożoności, nie jest nawet tak naprawdę szybka.
Aby szybko ocenić,, wartości są próbkowane w kilku punktach, a później stosowana jest tylko interpolacja liniowa. W rzeczywistości nie ma znaczenia, czy Φ jest dokładnie oszacowane, o ile przybliżenie jest ściśle monotoniczne.
Rozmiary pojemników dobiera się w taki sposób, aby ryzyko przepełnienia pojemnika było znikome. Mówiąc dokładniej, przy obecnych parametrach prawdopodobieństwo, że zestaw danych 50000000 elementów spowoduje przepełnienie kosza, wynosi 3,65e-09. (Może to być obliczony przy użyciu funkcji przeżycia na rozkład Poissona ).
Aby skompilować, użyj
Ponieważ obliczenia są znacznie większe niż w innych rozwiązaniach, te flagi kompilatora są potrzebne, aby uczynić je przynajmniej względnie szybkim. Bez
-msse3
konwersji zdouble
abyint
stać się naprawdę powoli. Jeśli twoja architektura nie obsługuje SSE3, konwersji tych można również dokonać za pomocąlrint()
funkcji.Kod jest raczej brzydki - nie jestem pewien, czy spełnia to wymaganie „rozsądnej czytelności” ...
źródło
Używa erf (), aby odpowiednio umieścić każdy element w koszu, a następnie sortuje każdy bin. Utrzymuje tablicę całkowicie na miejscu.
Pierwszy przebieg: docensus () zlicza liczbę elementów w każdym bin.
Drugie przejście: partition () zezwala na tablicę, umieszczając każdy element w odpowiednim bin
Trzecie przejście: sortbins () wykonuje qsort na każdym bin.
Jest to naiwne i wywołuje kosztowną funkcję erf () dwukrotnie dla każdej wartości. Pierwsze i trzecie przejście są potencjalnie równoległe. Drugi jest wysoce seryjny i prawdopodobnie jest spowolniony przez bardzo losowe wzorce dostępu do pamięci. Warto również buforować numer bin każdego podwójnego, w zależności od stosunku mocy procesora do prędkości pamięci.
Ten program pozwala wybrać liczbę używanych pojemników. Wystarczy dodać drugą liczbę do wiersza poleceń. Skompilowałem go z gcc -O3, ale moja maszyna jest tak słaba, że nie mogę powiedzieć ci żadnych dobrych wyników.
Edycja: Poof! Mój program C magicznie przekształcił się w program C ++ przy użyciu std :: sort!
źródło
Zobacz implementację sortowania radix autorstwa Michaela Herfa ( Radix Tricks ). Na mojej maszynie sortowanie było 5 razy szybsze w porównaniu z
std::sort
algorytmem z mojej pierwszej odpowiedzi. Nazwa funkcji sortowania toRadixSort11
.źródło