J'apprends Haskell et j'ai lu quelques articles concernant les différences de performances des listes Haskell et (insérez votre langue) les tableaux.
Étant un apprenant, j'utilise évidemment des listes sans même penser à la différence de performance. J'ai récemment commencé à enquêter et trouvé de nombreuses bibliothèques de structures de données disponibles dans Haskell.
Quelqu'un peut-il expliquer la différence entre les listes, les tableaux, les vecteurs, les séquences sans aller très loin dans la théorie informatique des structures de données?
En outre, existe-t-il des modèles courants dans lesquels vous utiliseriez une structure de données au lieu d'une autre?
Existe-t-il d'autres formes de structures de données qui me manquent et pourraient être utiles?
Réponses:
Listes Rock
De loin, la structure de données la plus conviviale pour les données séquentielles dans Haskell est la liste
Les listes vous donnent ϴ (1) contre et la correspondance des motifs. La bibliothèque standard, et d'ailleurs le prélude, est plein de fonctions de liste utiles qui devraient litière votre code (
foldr
,map
,filter
). Les listes sont persistantes , c'est-à-dire purement fonctionnelles, ce qui est très agréable. Les listes Haskell ne sont pas vraiment des "listes" car elles sont coinductives (d'autres langages appellent ces flux) donc des choses commefonctionne à merveille. Une infinité de structures de données rocheuses.
Les listes dans Haskell fournissent une interface un peu comme les itérateurs dans les langues impératives (à cause de la paresse). Il est donc logique qu'ils soient largement utilisés.
D'autre part
Le premier problème avec les listes est que leur indexation
(!!)
prend du temps ϴ (k), ce qui est ennuyeux. De plus, les ajouts peuvent être lents++
, mais le modèle d'évaluation paresseux de Haskell signifie que ceux-ci peuvent être traités comme entièrement amortis, s'ils se produisent.Le deuxième problème avec les listes est qu'elles ont une mauvaise localisation des données. Les vrais processeurs subissent des constantes élevées lorsque les objets en mémoire ne sont pas disposés côte à côte. Ainsi, en C ++
std::vector
a un "snoc" plus rapide (mettant les objets à la fin) que toute structure de données de liste liée pure que je connais, bien que ce ne soit pas une structure de données persistante donc moins conviviale que les listes de Haskell.Le troisième problème avec les listes est qu'elles ont une faible efficacité spatiale. Des tas de pointeurs supplémentaires augmentent votre stockage (par un facteur constant).
Les séquences sont fonctionnelles
Data.Sequence
est basé en interne sur des arbres à doigts (je sais, vous ne voulez pas le savoir) ce qui signifie qu'ils ont de belles propriétésData.Sequence
est une structure de données entièrement persistante.Data.Sequence
c'est tout au plus une constante plus lente.En revanche,
Data.Sequence
ne fait pas grand-chose pour le problème de localisation des données et ne fonctionne que pour les collections finies (c'est moins paresseux que les listes)Les tableaux ne sont pas pour les faibles de cœur
Les tableaux sont l'une des structures de données les plus importantes de CS, mais ils ne correspondent pas très bien au monde fonctionnel pur et paresseux. Les tableaux fournissent ϴ (1) un accès au milieu de la collection et des facteurs constants de localisation des données exceptionnellement bons. Mais, comme ils ne s'intègrent pas très bien dans Haskell, ils sont difficiles à utiliser. Il existe en fait une multitude de types de tableaux différents dans la bibliothèque standard actuelle. Il s'agit notamment de tableaux entièrement persistants, de tableaux mutables pour la monade IO, de tableaux mutables pour la monade ST et de versions non encadrées de ce qui précède. Pour en savoir plus, consultez le wiki haskell
Le vecteur est un "meilleur" tableau
Le
Data.Vector
package offre toutes les qualités du tableau, dans un niveau supérieur et une API plus propre. À moins que vous ne sachiez vraiment ce que vous faites, vous devez les utiliser si vous avez besoin de performances de type tableau. Bien sûr, certaines mises en garde s'appliquent toujours - les tableaux mutables comme les structures de données ne jouent tout simplement pas bien dans les langages paresseux purs. Pourtant, parfois vous voulez cette performance O (1), et vous laData.Vector
donne dans un package utilisable.Vous avez d'autres options
Si vous voulez juste des listes avec la capacité d'insérer efficacement à la fin, vous pouvez utiliser une liste de différences . Le meilleur exemple de listes qui bousillent les performances a tendance à provenir du
[Char]
préludeString
.Char
Les listes sont pratiques, mais ont tendance à fonctionner 20 fois plus lentement que les cordes C, alors n'hésitez pas à les utiliserData.Text
ou à les utiliser très rapidementData.ByteString
. Je suis sûr qu'il y a d'autres bibliothèques orientées séquences auxquelles je ne pense pas en ce moment.Conclusion
90 +% du temps j'ai besoin d'une collection séquentielle dans les listes Haskell sont la bonne structure de données. Les listes sont comme des itérateurs, les fonctions qui consomment des listes peuvent facilement être utilisées avec n'importe laquelle de ces autres structures de données en utilisant les
toList
fonctions fournies . Dans un monde meilleur, le prélude serait entièrement paramétrique quant au type de conteneur qu'il utilise, mais[]
jette actuellement la bibliothèque standard. Donc, utiliser des listes (presque) partout est définitivement correct.Vous pouvez obtenir des versions entièrement paramétriques de la plupart des fonctions de liste (et il est noble de les utiliser)
En fait,
Data.Traversable
définit une API qui est plus ou moins universelle sur tout ce qui "liste comme".Pourtant, bien que vous puissiez être bon et écrire uniquement du code entièrement paramétrique, la plupart d'entre nous ne le sont pas et utilisent la liste partout. Si vous apprenez, je vous suggère fortement de le faire aussi.
EDIT: D' après les commentaires que je réalise que je n'expliqué quand utiliser
Data.Vector
vsData.Sequence
. Les tableaux et les vecteurs fournissent des opérations d'indexation et de découpage extrêmement rapides, mais sont des structures de données fondamentalement transitoires (impératives). Les structures de données fonctionnelles pures aimentData.Sequence
et[]
permettent de produire efficacement de nouvelles valeurs à partir d'anciennes valeurs comme si vous aviez modifié les anciennes valeurs.ne modifie pas l'ancienne liste et n'a pas à la copier. Donc même si elle
oldList
est incroyablement longue, cette "modification" sera très rapide. De mêmeproduira une nouvelle séquence avec un
newValue
for à la place de son élément 3000. Encore une fois, cela ne détruit pas l'ancienne séquence, il en crée simplement une nouvelle. Mais, il le fait très efficacement, en prenant O (log (min (k, kn)) où n est la longueur de la séquence et k est l'indice que vous modifiez.Vous ne pouvez pas facilement le faire avec
Vectors
etArrays
. Ils peuvent être modifiés, mais c'est une véritable modification impérative, et ne peut donc pas être fait dans le code Haskell normal. Cela signifie que les opérations dans leVector
package qui apportent des modificationssnoc
etcons
doivent copier le vecteur entier prennent donc duO(n)
temps. La seule exception à cela est que vous pouvez utiliser la version mutable (Vector.Mutable
) à l'intérieur de laST
monade (ouIO
) et faire toutes vos modifications comme vous le feriez dans un langage impératif. Lorsque vous avez terminé, vous "gelez" votre vecteur pour le transformer en la structure immuable que vous souhaitez utiliser avec du code pur.Mon sentiment est que vous devriez utiliser par défaut
Data.Sequence
si une liste n'est pas appropriée. À utiliserData.Vector
uniquement si votre modèle d'utilisation n'implique pas de nombreuses modifications ou si vous avez besoin de performances extrêmement élevées dans les monades ST / IO.Si tout ce discours sur la
ST
monade vous laisse perplexe: raison de plus pour rester pur et rapide et beauData.Sequence
.la source
[1..]
liste dans Haskell. Les listes peuvent également être utilisées pour des choses amusantes comme le retour en arrière. Les considérer comme des structures de contrôle (en quelque sorte) a vraiment aidé à donner un sens à leur utilisation.Data.Sequence
. Les arbres à doigts sont l'une des inventions les plus impressionnantes de l'histoire de l'informatique (Guibas devrait probablement obtenir un prix Turing un jour) etData.Sequence
est une excellente mise en œuvre et dispose d'une API très utilisable.import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))
compile en une seule allocation de 404 octets (101 caractères) dans Core: hpaste.org/65015