Uczę się Haskell i czytam kilka artykułów dotyczących różnic w wydajności list Haskell i tablic (wstaw swój język).
Będąc uczniem, oczywiście po prostu używam list, nawet nie myśląc o różnicy w wydajności. Niedawno rozpocząłem badanie i znalazłem wiele bibliotek struktur danych dostępnych w Haskell.
Czy ktoś może wyjaśnić różnicę między listami, tablicami, wektorami, sekwencjami bez głębokiego zagłębiania się w informatyczną teorię struktur danych?
Czy są też jakieś typowe wzorce, w których można użyć jednej struktury danych zamiast innej?
Czy brakuje mi innych form struktur danych, które mogą być przydatne?
Odpowiedzi:
Listy Rock
Zdecydowanie najbardziej przyjazną strukturą danych sekwencyjnych w Haskell jest Lista
Listy dają ϴ (1) minusy i dopasowanie do wzorca. Standardowa biblioteka, a do tego znaczenia, preludium, jest pełna lista użytecznych funkcji, które powinien miot kod (
foldr
,map
,filter
). Listy są trwałe , aka czysto funkcjonalne, co jest bardzo miłe. Listy Haskell nie są tak naprawdę „listami”, ponieważ są koindukcyjne (inne języki nazywają te strumienie), więc takie rzeczypracować cudownie. Nieskończone struktury danych rock.
Listy w Haskell zapewniają interfejs podobny do iteratorów w imperatywnych językach (z powodu lenistwa). Ma więc sens, że są szeroko stosowane.
Z drugiej strony
Pierwszym problemem związanym z listami jest to, że ich indeksowanie
(!!)
zajmuje ϴ (k) czasu, co jest denerwujące. Dodatki mogą być również powolne++
, ale leniwy model oceny Haskella oznacza, że można je traktować jako w pełni zamortyzowane, jeśli w ogóle się zdarzają.Drugi problem z listami polega na tym, że mają słabą lokalizację danych. Rzeczywiste procesory mają wysokie stałe, gdy obiekty w pamięci nie są ułożone obok siebie. Tak więc w C ++
std::vector
ma szybsze „snoc” (umieszczanie obiektów na końcu) niż jakakolwiek znana struktura danych z listami połączonymi, o których wiem, chociaż nie jest to trwała struktura danych, która jest mniej przyjazna niż listy Haskella.Trzeci problem z listami polega na tym, że mają one mało miejsca. Wiązki dodatkowych wskaźników zwiększają pojemność pamięci (niezmiennie).
Sekwencje są funkcjonalne
Data.Sequence
jest wewnętrznie oparty na drzewkach palców (wiem, że nie chcesz tego wiedzieć), co oznacza, że mają pewne fajne właściwościData.Sequence
to w pełni trwała struktura danych.Data.Sequence
co najwyżej stały wolniej.Z drugiej strony
Data.Sequence
nie robi wiele dla problemu lokalizacji danych i działa tylko w przypadku kolekcji skończonych (jest mniej leniwy niż listy)Tablice nie są dla osób o słabym sercu
Tablice są jedną z najważniejszych struktur danych w CS, ale nie pasują bardzo dobrze do leniwego, czystego, funkcjonalnego świata. Tablice zapewniają ϴ (1) dostęp do środka kolekcji i wyjątkowo dobrą lokalizację danych / stałe czynniki. Ale ponieważ nie pasują bardzo dobrze do Haskell, używanie ich jest uciążliwe. W obecnej bibliotece standardowej istnieje wiele różnych typów tablic. Należą do nich w pełni trwałe tablice, zmienne tablice dla monady IO, modyfikowalne tablice dla monady ST i nieopakowane wersje powyższych. Aby uzyskać więcej informacji, odwiedź wiki haskell
Wektor jest „lepszym” zestawem
Data.Vector
Pakiet zawiera wszystkie dobroci tablicy w poziomie i czystszego wyższej API. O ile naprawdę nie wiesz, co robisz, powinieneś ich użyć, jeśli potrzebujesz wydajności podobnej do tablicy. Oczywiście nadal obowiązują pewne zastrzeżenia - zmienne tablice, takie jak struktury danych, po prostu nie działają dobrze w czysto leniwych językach. Mimo to czasami potrzebujesz wydajności O (1) iData.Vector
dajesz ją w użytecznym pakiecie.Masz inne opcje
Jeśli chcesz tylko listy z możliwością skutecznego wstawiania na końcu, możesz użyć listy różnic . Najlepszy przykład list zepsuć wydajność zwykle pochodzi z
[Char]
aliasu preludium jakoString
.Char
listy są wygodne, ale zwykle działają 20 razy wolniej niż łańcuchy C, więc możesz swobodnie używaćData.Text
lub bardzo szybkoData.ByteString
. Jestem pewien, że istnieją inne biblioteki zorientowane na sekwencje, o których teraz nie myślę.Wniosek
Ponad 90% czasu potrzebuję sekwencyjnego gromadzenia na listach Haskella odpowiedniej struktury danych. Listy są jak iteratory, funkcje wykorzystujące listy mogą być łatwo używane z dowolną z tych innych struktur danych przy użyciu
toList
funkcji, które są z nimi związane. W lepszym świecie preludium byłoby w pełni parametryczne co do typu używanego kontenera, ale obecnie[]
zaśmieca standardową bibliotekę. Tak więc używanie list (prawie) w każdym miejscu jest zdecydowanie w porządku.Możesz uzyskać w pełni parametryczne wersje większości funkcji listy (i możesz z nich korzystać)
W rzeczywistości
Data.Traversable
definiuje interfejs API, który jest mniej więcej uniwersalny w każdej „liście takiej jak”.Mimo to, chociaż potrafisz być dobry i pisać tylko w pełni parametryczny kod, większość z nas nie jest i używa listy w dowolnym miejscu. Jeśli się uczysz, zdecydowanie też to robisz.
EDIT: Na podstawie wypowiedzi zdaję sobie sprawę, że nigdy nie wyjaśnił, kiedy użyć
Data.Vector
vsData.Sequence
. Macierze i wektory zapewniają niezwykle szybkie operacje indeksowania i dzielenia, ale są zasadniczo przejściowymi (imperatywnymi) strukturami danych. Czyste funkcjonalne struktury danych, takie jakData.Sequence
i[]
pozwalają wydajnie generować nowe wartości ze starych wartości, tak jakbyś zmodyfikował stare wartości.nie modyfikuje starej listy i nie musi jej kopiować. Więc nawet jeśli
oldList
jest niewiarygodnie długi, ta „modyfikacja” będzie bardzo szybka. podobniestworzy nową sekwencję z
newValue
for zamiast zamiast 3000 elementów. Ponownie nie niszczy starej sekwencji, po prostu tworzy nową. Ale robi to bardzo skutecznie, biorąc O (log (min (k, kn)), gdzie n jest długością sekwencji, a k jest zmodyfikowanym indeksem.Nie możesz tego łatwo zrobić za pomocą
Vectors
iArrays
. Można je modyfikować, ale jest to prawdziwa konieczność, dlatego nie można tego zrobić w zwykłym kodzie Haskell. Oznacza to operacje wVector
pakiecie, które dokonują modyfikacjisnoc
icons
muszą kopiować cały wektor, więc poświęć trochęO(n)
czasu. Jedynym wyjątkiem jest to, że można używać wersji mutable (Vector.Mutable
) wewnątrzST
monady (lubIO
) i wykonywać wszystkie modyfikacje tak, jak w trybie rozkazującym. Kiedy skończysz, „zamrażasz” wektor, aby przekształcić się w niezmienną strukturę, której chcesz używać z czystym kodem.Mam wrażenie, że powinieneś domyślnie używać,
Data.Sequence
jeśli lista nie jest odpowiednia. UżywajData.Vector
tylko wtedy, gdy wzorzec użytkowania nie wymaga wielu modyfikacji lub jeśli potrzebujesz wyjątkowo wysokiej wydajności w obrębie monad ST / IO.Jeśli całe to gadanie o
ST
monadzie wprawia cię w zakłopotanie: tym bardziej powód, by trzymać się czysto szybko i pięknieData.Sequence
.źródło
[1..]
listy w języku Haskell. Listy mogą być również wykorzystywane do zabawnych rzeczy, takich jak cofanie się. Myślenie o nich jako o strukturach kontrolnych (w pewnym sensie) naprawdę pomogło zrozumieć, w jaki sposób są używane.Data.Sequence
. Paluszki są jednym z najbardziej niesamowitych wynalazków w historii komputerów (Guibas prawdopodobnie powinien kiedyś otrzymać nagrodę Turinga) iData.Sequence
jest doskonałą implementacją i ma bardzo użyteczny interfejs API.import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))
kompiluje się do pojedynczej alokacji 404 bajtów (101 znaków) w rdzeniu: hpaste.org/65015