Quand dois-je choisir Vector dans Scala?

200

Il semble que Vectorc'était tard pour la soirée des collections Scala, et tous les articles de blog influents étaient déjà partis.

En Java, ArrayListc'est la collection par défaut - je pourrais l'utiliser, LinkedListmais seulement lorsque j'aurai réfléchi à un algorithme et que je me soucierai suffisamment pour l'optimiser. Dans Scala, dois-je utiliser Vectorpar défaut Seqou essayer de déterminer quand Listest-ce réellement plus approprié?

Duncan McGregor
la source
1
Je suppose que ce que je veux dire ici, c'est qu'en Java, je List<String> l = new ArrayList<String>()créerais des blogs Scala pour vous faire croire que tout le monde utilise List pour obtenir une qualité de collection persistante - mais Vector est-il suffisamment polyvalent pour que nous devrions l'utiliser à la place de List?
Duncan McGregor
9
@Debilski: Je me demande ce que vous entendez par là. J'obtiens un Listlorsque je tape Seq()à REPL.
missingfaktor
1
Hmm, eh bien, c'est dit dans les documents. Peut-être que cela n'est vrai que pour IndexedSeq.
Debilski
1
Le commentaire concernant le type de béton par défaut date de Seqplus de trois ans. Depuis Scala 2.11.4 (et versions antérieures), le type concret par défaut de Seqest List.
Mark Canlas
3
Pour un accès aléatoire, le vecteur est meilleur. Pour la tête, l'accès à la queue, la liste est meilleure. Pour les opérations en masse, telles que carte, filtre, vecteur est préféré car le vecteur est organisé avec 32 éléments en tant que bloc tandis que la liste organise les éléments avec des pointeurs les uns aux autres, il n'y a aucune garantie que ces éléments sont proches les uns des autres.
johnsam

Réponses:

280

En règle générale, utilisez par défaut Vector. C'est plus rapide que Listpour presque tout et plus efficace en mémoire pour les séquences de taille plus grande que triviale. Consultez cette documentation sur les performances relatives de Vector par rapport aux autres collections. Il y a des inconvénients à y aller Vector. Plus précisément:

  • Les mises à jour en tête sont plus lentes que List(mais pas autant que vous le pensez)

Un autre inconvénient avant Scala 2.10 était que le support de correspondance de modèles était meilleur List, mais cela a été corrigé en 2.10 avec des généralisateurs +:et des :+extracteurs.

Il existe également une manière plus abstraite et algébrique d'aborder cette question: quelle sorte de séquence avez-vous conceptuellement ? De plus, qu'en faites-vous conceptuellement ? Si je vois une fonction qui renvoie unOption[A] , je sais que cette fonction a quelques trous dans son domaine (et est donc partielle). Nous pouvons appliquer cette même logique aux collections.

Si j'ai une séquence de type List[A], j'affirme effectivement deux choses. Premièrement, mon algorithme (et mes données) est entièrement structuré en pile. Deuxièmement, j'affirme que les seules choses que je vais faire avec cette collection sont des traversées complètes O (n). Ces deux vont vraiment de pair. Inversement, si j'ai quelque chose de type Vector[A], la seule chose que j'affirme, c'est que mes données ont un ordre bien défini et une longueur finie. Ainsi, les affirmations sont plus faibles avec Vector, et cela conduit à une plus grande flexibilité.

Daniel Spiewak
la source
2
2.10 est sorti depuis un moment maintenant, le modèle de liste correspond-il toujours mieux que Vector?
Tim Gautier
3
La correspondance des modèles de liste n'est plus meilleure. En fait, c'est tout le contraire. Par exemple, pour avoir la tête et la queue on peut faire case head +: tailou case tail :+ head. Pour faire correspondre contre vide, vous pouvez faire case Seq()et ainsi de suite. Tout ce dont vous avez besoin est là dans l'API, qui est plus polyvalent que Listle s
Kai Sellgren
Listest implémenté avec une liste à liaison unique. Vectorest implémenté quelque chose comme Java ArrayList.
Josiah Yoder du
6
@JosiahYoder Il n'est rien implémenté comme ArrayList. ArrayList encapsule un tableau qu'il redimensionne dynamiquement. Le vecteur est un trie , où les clés sont les index des valeurs.
John Colanduoni
1
Je m'excuse. J'allais sur une source Web qui était vague sur les détails. Dois-je corriger ma déclaration précédente? Ou est-ce une mauvaise forme?
Josiah Yoder du
93

Eh bien, a Listpeut être incroyablement rapide si l'algorithme peut être implémenté uniquement avec ::, headet tail. J'ai eu une leçon d'objet très récemment, quand j'ai battu Java spliten générant un Listau lieu d'un Array, et je ne pouvais pas battre ça avec autre chose.

Cependant, Lista un problème fondamental: il ne fonctionne pas avec des algorithmes parallèles. Je ne peux pas diviser un Listen plusieurs segments, ni le concaténer de manière efficace.

Il existe d'autres types de collections qui peuvent mieux gérer le parallélisme - et Vectorc'est l'un d'entre eux. Vectora également une grande localité - ce qui Listn'est pas le cas - ce qui peut être un vrai plus pour certains algorithmes.

Donc, tout bien considéré, Vectorest le meilleur choix, sauf si vous avez des considérations spécifiques qui rendent l'une des autres collections préférable - par exemple, vous pouvez choisir Streamsi vous voulez une évaluation et une mise en cache paresseuses ( Iteratorest plus rapide mais ne met pas en cache), ou Listsi l'algorithme est naturellement implémenté avec les opérations que j'ai mentionnées.

Soit dit en passant, il est préférable d'utiliser Seqou à IndexedSeqmoins que vous ne vouliez un morceau spécifique d'API (comme celui Listde ::), ou même GenSeqou GenIndexedSeqsi votre algorithme peut être exécuté en parallèle.

Daniel C. Sobral
la source
3
Merci d'avoir répondu. Qu'entendez-vous par «a une grande localité»?
Ngoc Dao
10
@ngocdaothanh Cela signifie que les données sont regroupées étroitement dans la mémoire, ce qui améliore les chances que les données soient dans le cache lorsque vous en avez besoin.
Daniel C.Sobral
1
@ user247077 Oui, les listes peuvent battre les vecteurs en termes de performances compte tenu des détails que j'ai mentionnés. Et toutes les actions des vecteurs ne sont pas amorties O (1). En fait, sur les structures de données immuables (ce qui est le cas), les insertions / suppressions alternatives à chaque extrémité ne seront pas amorties du tout. Dans ce cas, le cache est inutile car vous copiez toujours le vecteur.
Daniel C.Sobral
1
@ user247077 Peut-être n'êtes-vous pas conscient qu'il Vectorexiste une structure de données immuable dans Scala?
Daniel C.Sobral
1
@ user247077 C'est beaucoup plus compliqué que cela, y compris des trucs mutables en interne pour rendre l'ajout moins cher, mais lorsque vous l'utilisez comme une pile, ce qui est un scénario optimal de liste immuable, vous finissez toujours par avoir les mêmes caractéristiques de mémoire qu'une liste liée, mais avec un profil d'allocation de mémoire beaucoup plus grand.
Daniel C.Sobral
29

Certaines des déclarations ici sont déroutantes ou même fausses, en particulier l'idée qu'immuable.Vector dans Scala est quelque chose comme une ArrayList. La liste et le vecteur sont tous deux des structures de données immuables et persistantes (c'est-à-dire «bon marché pour obtenir une copie modifiée»). Il n'y a pas de choix par défaut raisonnable car il pourrait s'agir de structures de données mutables, mais cela dépend plutôt de ce que fait votre algorithme. List est une liste liée individuellement, tandis que Vector est un trie de base 32, c'est-à-dire qu'il s'agit d'une sorte d'arbre de recherche avec des nœuds de degré 32. En utilisant cette structure, Vector peut fournir les opérations les plus courantes raisonnablement rapidement, c'est-à-dire en O (log_32 ( n)). Cela fonctionne pour le préfixe, l'ajout, la mise à jour, l'accès aléatoire, la décomposition en tête / queue. L'itération dans un ordre séquentiel est linéaire. La liste, en revanche, fournit juste une itération linéaire et un pré-ajout à temps constant, une décomposition en tête / queue.

Cela pourrait ressembler à si Vector était un bon remplacement pour List dans presque tous les cas, mais le préfixe, la décomposition et l'itération sont souvent les opérations cruciales sur les séquences d'un programme fonctionnel, et les constantes de ces opérations sont (beaucoup) plus élevées pour le vecteur dû à sa structure plus compliquée. J'ai fait quelques mesures, donc l'itération est environ deux fois plus rapide pour la liste, le préfixe est environ 100 fois plus rapide sur les listes, la décomposition en tête / queue est environ 10 fois plus rapide sur les listes et la génération à partir d'un objet traversable est environ 2 fois plus rapide pour les vecteurs. (C'est probablement parce que Vector peut allouer des tableaux de 32 éléments à la fois lorsque vous le créez à l'aide d'un générateur au lieu d'ajouter ou d'ajouter des éléments un par un).

Alors, quelle structure de données devrions-nous utiliser? Fondamentalement, il existe quatre cas courants:

  • Nous avons seulement besoin de transformer des séquences par des opérations comme map, filter, fold etc: fondamentalement cela n'a pas d'importance, nous devons programmer notre algorithme de manière générique et pourrait même bénéficier d'accepter des séquences parallèles. Pour les opérations séquentielles, la liste est probablement un peu plus rapide. Mais vous devriez le comparer si vous devez l'optimiser.
  • Nous avons besoin de beaucoup d'accès aléatoire et de mises à jour différentes, nous devons donc utiliser le vecteur, la liste sera extrêmement lente.
  • Nous opérons sur les listes de manière fonctionnelle classique, en les construisant en ajoutant et en itérant par décomposition récursive: utilisez la liste, le vecteur sera plus lent d'un facteur 10-100 ou plus.
  • Nous avons un algorithme critique de performance qui est fondamentalement impératif et fait beaucoup d'accès aléatoire sur une liste, quelque chose comme un tri rapide en place: utilisez une structure de données impérative, par exemple ArrayBuffer, localement et copiez vos données depuis et vers elle.
dth
la source
24

Pour les collections immuables, si vous voulez une séquence, votre décision principale est d'utiliser IndexedSeqou non LinearSeq, ce qui donne des garanties de performances différentes. Un IndexedSeq fournit un accès aléatoire rapide aux éléments et une opération de longueur rapide. Un LinearSeq fournit un accès rapide uniquement au premier élément via head, mais a également un tailfonctionnement rapide . (Tiré de la documentation Seq.)

Pour un, IndexedSeqvous choisiriez normalement un Vector. Ranges et WrappedStrings sont également IndexedSeqs.

Pour un, LinearSeqvous choisirez normalement un Listou son équivalent paresseux Stream. D'autres exemples sont Queues et Stacks.

Donc, en termes Java, ArrayListutilisé de manière similaire à Scala Vector, et de LinkedListmanière similaire à Scala List. Mais dans Scala, j'aurais tendance à utiliser List plus souvent que Vector, car Scala a un bien meilleur support pour les fonctions qui incluent la traversée de la séquence, comme le mappage, le pliage, l'itération, etc. Vous aurez tendance à utiliser ces fonctions pour manipuler la liste comme un plutôt que d'accéder de manière aléatoire à des éléments individuels.

Luigi Plinge
la source
Mais si l'itération de Vector est plus rapide que celle de List, et je peux également mapper fold etc, alors à part certains cas spécialisés (essentiellement tous ces algorithmes FP spécialisés pour List), il semble que List soit essentiellement hérité.
Duncan McGregor
@Duncan où avez-vous entendu que l'itération de Vector est plus rapide? Pour commencer, vous devez suivre et mettre à jour l'index actuel, ce dont vous n'avez pas besoin avec une liste chaînée. Je n'appellerais pas les fonctions de liste des «cas spécialisés» - elles sont le pain et le beurre de la programmation fonctionnelle. Ne pas les utiliser reviendrait à essayer de programmer Java sans boucles for ou while.
Luigi Plinge
2
Je suis sûr que l Vector'itération est plus rapide, mais quelqu'un doit le comparer pour en être sûr.
Daniel Spiewak
Je pense que (?) Les éléments en Vectorexistent physiquement ensemble sur la RAM en groupes de 32, qui s'intègrent plus pleinement dans le cache CPU ... donc il y a moins de manque de cache
richizy
2

Dans les situations qui impliquent beaucoup d'accès aléatoire et de mutation aléatoire, un Vector(ou - comme le disent les docs - un Seq) semble être un bon compromis. C'est également ce que suggèrent les caractéristiques de performance .

De plus, la Vectorclasse semble bien jouer dans des environnements distribués sans beaucoup de duplication de données car il n'est pas nécessaire de faire une copie sur écriture pour l'objet complet. (Voir: http://akka.io/docs/akka/1.1.3/scala/stm.html#persistent-datastructures )

Debilski
la source
1
Il y a tant à apprendre ... Que signifie Vector comme Seq par défaut? Si j'écris Seq (1, 2, 3), j'obtiens List [Int] et non Vector [Int].
Duncan McGregor
2
Si vous avez un accès aléatoire, utilisez un IndexedSeq. Ce qui l'est aussi Vector, mais c'est une autre affaire.
Daniel C.Sobral
@DuncanMcGregor: Vector est la valeur par défaut IndexedSeqqui implémente Seq. Seq(1, 2, 3)est un LinearSeqqui est implémenté en utilisant List.
pathikrit
0

Si vous programmez immuablement et avez besoin d'un accès aléatoire, Seq est le chemin à parcourir (sauf si vous voulez un Set, ce que vous faites souvent). Sinon, List fonctionne bien, sauf que ses opérations ne peuvent pas être parallélisées.

Si vous n'avez pas besoin de structures de données immuables, restez avec ArrayBuffer car c'est l'équivalent Scala d'ArrayList.

Joshua Hartman
la source
Je m'en tiens au domaine des collections immuables et persistantes. Mon point est que, même si je n'ai pas besoin d'un accès aléatoire, Vector a-t-il effectivement remplacé List?
Duncan McGregor
2
Cela dépend un peu du cas d'utilisation. Les vecteurs sont plus équilibrés. L'itération est plus rapide que la liste et l'accès aléatoire est beaucoup plus rapide. Les mises à jour sont plus lentes car ce n'est pas seulement une liste pré-ajoutée, sauf s'il s'agit d'une mise à jour groupée à partir d'un pli qui peut être effectuée avec un générateur. Cela dit, je pense que Vector est le meilleur choix par défaut car il est si polyvalent.
Joshua Hartman
Je pense que cela va au cœur de ma question - les vecteurs sont si bons que nous pouvons aussi bien les utiliser là où les exemples montrent généralement List.
Duncan McGregor