Quando devo escolher Vector no Scala?

200

Parece que Vector estava atrasado para a festa das coleções Scala, e todas as publicações influentes do blog já haviam saído.

Em Java ArrayListé a coleção padrão - eu poderia usar, LinkedListmas apenas quando eu pensei em um algoritmo e me preocupei o suficiente para otimizar. No Scala, devo usar Vectorcomo padrão Seqou tentar descobrir quando Listé realmente mais apropriado?

Duncan McGregor
fonte
1
Acho que o que quero dizer aqui é que, em Java, eu criaria List<String> l = new ArrayList<String>()blogs Scala de gravação , para que você acreditasse que todo mundo usa a List para obter uma qualidade persistente da coleção - mas o Vector é de uso geral o suficiente para usá-lo no lugar da List?
Duncan McGregor
9
@Debilski: Eu estou querendo saber o que você quer dizer com isso. Recebo um Listquando digito Seq()no REPL.
missingfaktor
1
Hmm, bem, isso diz nos documentos. Talvez isso seja verdade apenas para IndexedSeq.
Debilski
1
O comentário sobre o tipo de concreto padrão Seqtem mais de três anos. No Scala 2.11.4 (e anterior), o tipo concreto padrão de Seqé List.
precisa
3
Para acesso aleatório, o vetor é melhor. Para acesso de cabeça e cauda, ​​a lista é melhor. Para operações em massa, como mapa, filtro, vetor é preferível, pois o vetor é organizado com 32 elementos como um pedaço, enquanto a lista organizou os elementos com ponteiros entre si, não há garantia de que esses elementos estejam próximos um do outro.
johnsam

Respostas:

280

Como regra geral, o padrão é usar Vector. É mais rápido que Listem quase tudo e mais eficiente em memória para sequências de tamanho maior que trivial. Consulte esta documentação do desempenho relativo do vetor em comparação com as outras coleções. Existem algumas desvantagens em seguir Vector. Especificamente:

  • As atualizações na cabeça são mais lentas do que List(embora não tanto quanto você imagina)

Outra desvantagem antes do Scala 2.10 foi que o suporte à correspondência de padrões era melhor List, mas isso foi corrigido no 2.10 com generalização +:e:+ extratores.

Há também uma maneira mais abstrata e algébrica de abordar essa questão: que tipo de sequência você tem conceitualmente ? Além disso, o que você é fazendo conceitualmente com isso? Se eu vir uma função que retorna um Option[A], sei que ela possui alguns buracos em seu domínio (e, portanto, é parcial). Podemos aplicar essa mesma lógica às coleções.

Se eu tenho uma sequência do tipo List[A], estou afirmando efetivamente duas coisas. Primeiro, meu algoritmo (e dados) é totalmente estruturado em pilha. Segundo, estou afirmando que as únicas coisas que farei com esta coleção são cheias, O (n) travessias. Esses dois realmente andam de mãos dadas. Por outro lado, se eu tiver algo do tipo Vector[A], a única coisa que afirmo é que meus dados têm uma ordem bem definida e um comprimento finito. Assim, as afirmações são mais fracas Vectore isso leva a uma maior flexibilidade.

Daniel Spiewak
fonte
2
2.10 já está fora há algum tempo, o padrão de lista ainda é melhor que Vector?
Tim Gautier
3
A correspondência de padrões de lista não é mais melhor. De fato, é bem o contrário. Por exemplo, para obter cabeça e cauda, ​​pode-se fazer case head +: tailou case tail :+ head. Para combinar com o vazio, você pode fazer case Seq()e assim por diante. Tudo que você precisa está lá na API, que é mais versátil do que List's
Kai Sellgren
Listé implementado com uma lista vinculada individualmente. Vectoré implementado algo como o Java ArrayList.
21815 Josiah Yoder
6
@ JosiahYoder Não é implementado nada como ArrayList. ArrayList agrupa uma matriz que redimensiona dinamicamente. Vector é um trie , onde as chaves são os índices de valores.
John Colanduoni
1
Peço desculpas. Eu estava acessando uma fonte da Web que era vaga sobre os detalhes. Devo corrigir minha declaração anterior? Ou isso é ruim?
Josiah Yoder 10/09
93

Bem, a Listpode ser incrivelmente rápido se o algoritmo puder ser implementado apenas com ::, heade tail. Eu tive uma lição objetiva disso muito recentemente, quando venci o Java splitgerando um em Listvez de um Array, e não consegui vencê-lo com mais nada.

No entanto, Listtem um problema fundamental: não funciona com algoritmos paralelos. Não posso dividir um Listem vários segmentos ou concatená-lo novamente, de maneira eficiente.

Existem outros tipos de coleções que conseguem lidar com o paralelismo muito melhor - e Vectoré um deles. Vectortambém possui ótima localidade - o que Listnão ocorre - o que pode ser uma vantagem real para alguns algoritmos.

Portanto, considerando todas as coisas, Vectoré a melhor opção, a menos que você tenha considerações específicas que tornam uma das outras coleções preferíveis - por exemplo, você pode escolher Streamse deseja uma avaliação e cache preguiçosos ( Iteratoré mais rápido, mas não faz cache), ouList se o algoritmo é naturalmente implementado com as operações que mencionei.

By the way, é preferível usar Seqou IndexedSeqa menos que você quer um pedaço específico de API (como List's ::), ou mesmo GenSeqou GenIndexedSeqse o seu algoritmo pode ser executado em paralelo.

Daniel C. Sobral
fonte
3
Obrigado pela resposta. O que você quer dizer com "tem ótima localidade"?
Ngoc Dao
10
@ngocdaothanh Significa que os dados são agrupados na memória, aumentando a chance de que os dados estejam no cache quando você precisar.
Daniel C. Sobral
1
@ user247077 Sim, as listas podem superar os Vetores no desempenho, dadas as informações que mencionei. E nem todas as ações dos vetores são amortizadas O (1). De fato, em estruturas de dados imutáveis ​​(que é o caso), inserções / exclusões alternativas em cada extremidade não serão amortizadas. Nesse caso, o cache é inútil porque você está sempre copiando o vetor.
Daniel C. Sobral
1
@ user247077 Talvez você não esteja ciente de que Vectoré uma estrutura de dados imutável no Scala?
Daniel C. Sobral
1
@ user247077 É muito mais complicado que isso, incluindo algumas coisas mutáveis ​​internamente para tornar o acréscimo mais barato, mas quando você o usa como uma pilha, cenário ideal para a lista imutável, você ainda acaba tendo as mesmas características de memória de uma lista vinculada, mas com um perfil de alocação de memória muito maior.
Daniel C. Sobral
29

Algumas das declarações aqui são confusas ou até erradas, especialmente a ideia de que é imutável. O vetor em Scala é semelhante a um ArrayList. List e Vector são estruturas de dados imutáveis ​​e persistentes (isto é, "baratas para obter uma cópia modificada"). Não existe uma opção padrão razoável, como pode ser para estruturas de dados mutáveis, mas depende do que o seu algoritmo está fazendo. List é uma lista vinculada individualmente, enquanto Vector é um número inteiro de base 32, ou seja, é um tipo de árvore de pesquisa com nós de grau 32. Usando essa estrutura, o Vector pode fornecer operações mais comuns razoavelmente rápidas, ou seja, em O (log_32 ( n)). Isso funciona para pré-acrescentar, acrescentar, atualizar, acesso aleatório, decomposição na cabeça / cauda. A iteração em ordem seqüencial é linear. A lista, por outro lado, apenas fornece iteração linear e pré-tempo constante, decomposição na cabeça / cauda.

Pode parecer que o vetor substitui a lista em quase todos os casos, mas o prefixo, a decomposição e a iteração são frequentemente as operações cruciais nas seqüências de um programa funcional, e as constantes dessas operações são (muito) maiores para o vetor devido a à sua estrutura mais complicada. Fiz algumas medições, para que a iteração seja cerca de duas vezes mais rápida para a lista, o prefixo é cerca de 100 vezes mais rápido nas listas, a decomposição na cabeça / cauda é cerca de 10 vezes mais rápida nas listas e a geração de um traversable é cerca de 2 vezes mais rápida para vetores. (Provavelmente, porque o Vector pode alocar matrizes de 32 elementos ao mesmo tempo quando você o cria usando um construtor, em vez de acrescentar ou acrescentar elementos um por um).

Então, qual estrutura de dados devemos usar? Basicamente, existem quatro casos comuns:

  • Nós só precisamos transformar sequências por operações como mapear, filtrar, dobrar, etc: basicamente, não importa, devemos programar nosso algoritmo genericamente e até nos beneficiar da aceitação de sequências paralelas. Para operações seqüenciais, a lista é provavelmente um pouco mais rápida. Mas você deve compará-lo se precisar otimizar.
  • Precisamos de muito acesso aleatório e atualizações diferentes, portanto devemos usar o vetor, a lista será proibitivamente lenta.
  • Operamos em listas de uma maneira funcional clássica, construindo-as pré-anexando e iterando por decomposição recursiva: lista de uso, o vetor será mais lento por um fator de 10 a 100 ou mais.
  • Temos um algoritmo de desempenho crítico que é basicamente imperativo e faz muito acesso aleatório em uma lista, algo como uma classificação rápida no local: use uma estrutura de dados imperativa, por exemplo, ArrayBuffer, localmente e copie seus dados de e para ele.
dth
fonte
24

Para coleções imutáveis, se você deseja uma sequência, sua principal decisão é usar um IndexedSeqou a LinearSeq, o que fornece garantias diferentes de desempenho. Um IndexedSeq fornece acesso aleatório rápido a elementos e uma operação rápida. Um LinearSeq fornece acesso rápido apenas ao primeiro elemento via head, mas também possui uma tailoperação rápida . (Retirado da documentação Seq.)

Para um, IndexedSeqvocê normalmente escolheria um Vector. Ranges e WrappedStrings também são IndexedSeqs.

Para um, LinearSeqvocê normalmente escolheria um Listou seu equivalente preguiçoso Stream. Outros exemplos são Queues e Stacks.

Assim, em termos de Java, ArrayListusado da mesma forma que o Scala Vectore LinkedListda Scala List. Mas no Scala eu tenderia a usar a List com mais frequência do que Vector, porque o Scala tem um suporte muito melhor para funções que incluem a travessia da sequência, como mapeamento, dobragem, iteração etc. Você tenderá a usar essas funções para manipular a lista como um vetor. todo, em vez de acessar aleatoriamente elementos individuais.

Luigi Plinge
fonte
Mas se a iteração de Vector é mais rápida que a de List e também posso mapear fold etc., além de alguns casos especializados (essencialmente todos os algoritmos FP especializados em List), parece que List é essencialmente legado.
Duncan McGregor
@Duncan, onde você ouviu que a iteração do Vector é mais rápida? Para começar, você precisa acompanhar e atualizar o índice atual, o que não é necessário em uma lista vinculada. Eu não chamaria as funções de lista de "casos especializados" - eles são o pão com manteiga da programação funcional. Não usá-los seria como tentar programar Java sem loops for ou while.
Luigi Plinge
2
Tenho certeza de que Vectora iteração é mais rápida, mas alguém precisa compará-la para ter certeza.
Daniel Spiewak
(?) Eu acho que elementos Vectorexistem fisicamente juntos em RAM em grupos de 32, que se encaixam mais plenamente no cache da CPU ... por isso há menos perda de cache
richizy
2

Em situações que envolvem muito acesso aleatório e mutação aleatória, um Vector(ou - como dizem os médicos - a Seq) parece ser um bom compromisso. Isso também é o que as características de desempenho sugerem.

Além disso, a Vectorclasse parece funcionar bem em ambientes distribuídos sem muita duplicação de dados, porque não há necessidade de fazer uma cópia na gravação para o objeto completo. (Consulte: http://akka.io/docs/akka/1.1.3/scala/stm.html#persistent-datastructures )

Debilski
fonte
1
Tanto para aprender ... O que significa Vector sendo o padrão Seq? Se eu escrever Seq (1, 2, 3), recebo List [Int] e não Vector [Int].
Duncan McGregor
2
Se você tiver acesso aleatório, use um IndexedSeq. O que também é Vector, mas isso é outra questão.
Daniel C. Sobral
@DuncanMcGregor: Vector é o padrão IndexedSeqque implementa Seq. Seq(1, 2, 3)é um LinearSeqque é implementado usando List.
Pathikrit
0

Se você está programando imutável e precisa de acesso aleatório, o Seq é o caminho a seguir (a menos que você queira um conjunto, o que geralmente é necessário). Caso contrário, a Lista funciona bem, exceto que suas operações não podem ser paralelizadas.

Se você não precisar de estruturas de dados imutáveis, fique com o ArrayBuffer, pois é o equivalente do Scala ao ArrayList.

Joshua Hartman
fonte
Estou aderindo ao reino de coleções imutáveis ​​e persistentes. Meu argumento é que, mesmo que eu não precise de acesso aleatório, o Vector substituiu efetivamente a Lista?
Duncan McGregor
2
Depende um pouco do caso de uso. Os vetores são mais equilibrados. A iteração é mais rápida que a lista e o acesso aleatório é muito mais rápido. As atualizações são mais lentas, pois não se trata apenas de um prefixo de lista, a menos que seja uma atualização em massa de uma dobra que pode ser feita com um construtor. Dito isto, acho que Vector é a melhor opção padrão, pois é muito versátil.
Joshua Hartman
Que eu acho que chega ao cerne da minha pergunta - os vetores são tão bons que podemos usá-los onde os exemplos geralmente mostram List.
Duncan McGregor