Qual é o sentido de usar listas sobre vetores, em C ++?


32

Realizei 3 experimentos diferentes envolvendo listas e vetores C ++.

Aqueles com vetores se mostraram mais eficientes, mesmo quando muitas inserções no meio estavam envolvidas.

Daí a pergunta: em qual caso as listas fazem mais sentido que os vetores?

Se os vetores parecem mais eficientes na maioria dos casos, e considerando como são seus membros, quais são as vantagens deixadas para as listas?

  1. Gere N números inteiros e coloque-os em um contêiner para que o contêiner permaneça classificado. A inserção foi realizada ingenuamente, lendo os elementos um a um e inserindo o novo logo antes do primeiro maior.
    Com uma lista, o tempo passa pelo teto quando a dimensão aumenta, em comparação com os vetores.

  2. Insira N números inteiros no final do contêiner.
    Para listas e vetores, o tempo aumentou na mesma ordem de magnitude, embora tenha sido três vezes mais rápido com vetores.

  3. Inserir N números inteiros em um contêiner.
    Iniciar temporizador.
    Classifique o contêiner usando list.sort para listas e std :: sort para vetores. Pare o cronômetro.
    Novamente, o tempo aumenta na mesma ordem de magnitude, mas é em média 5 vezes mais rápido com vetores.

Eu posso continuar realizando testes e descobrir alguns exemplos em que as listas seriam melhores.

Mas a experiência conjunta de vocês lendo esta mensagem pode fornecer respostas mais produtivas.

Você pode ter encontrado situações em que as listas eram mais convenientes de usar ou tiveram um desempenho melhor?


2
Você deve dar uma olhada em Quando usar uma lista vinculada em uma matriz / lista de matrizes? se você já não tiver
Karthik T

1
Aqui está outro bom recurso sobre o assunto: stackoverflow.com/a/2209564/8360 Além disso, a maioria das orientações sobre C ++ que ouvi é usar o vetor por padrão, listar apenas se você tiver um motivo específico.
Zachary Yates

Obrigado. No entanto, não concordo com a maior parte do que é dito na resposta favorita. A maioria dessas idéias preconcebidas foi invalidada pelas minhas experiências. Essa pessoa não fez nenhum teste e aplicou a teoria difundida ensinada nos livros ou na escola.
Marek Stanley

1
Um listprovavelmente faz melhor se você está removendo muitos elementos. Eu não acredito que uma vectorvez retorne memória ao sistema até que todo o vetor seja excluído. Lembre-se também de que seu teste nº 1 não está testando apenas o tempo de inserção. É um teste que combina pesquisa e inserção. É encontrar o lugar para inserir onde listé lento. A inserção real será mais rápida que o vetor.
Gort the Robot

3
É tão típico que esta pergunta é descrita em termos de (tempo de execução) desempenho, desempenho e somente desempenho. Isso parece ser um ponto cego para muitos programadores - eles se concentram nesse aspecto e esquecem que existem dezenas de outros aspectos que geralmente são muito, muito mais importantes.
Doc Brown

Respostas:


34

A resposta curta é que os casos parecem poucos e distantes entre si. Provavelmente existem alguns.

Uma seria quando você precisa armazenar um pequeno número de objetos grandes - especialmente objetos que são tão grandes que é impraticável alocar espaço para até alguns extras. Basicamente, não há como impedir que um vetor ou deque aloque espaço para objetos extras - é assim que eles são definidos (ou seja, eles devem alocar espaço extra para atender aos requisitos de complexidade). Se você não puder permitir que esse espaço extra seja alocado, std::listtalvez seja o único contêiner padrão que atenda às suas necessidades.

Outra seria quando / se você armazenará um iterador em um ponto "interessante" em uma lista por um longo período de tempo e, ao fazer inserções e / ou exclusões, você (quase) sempre o faz de um local para o qual você já possui um iterador, portanto, não percorre a lista para chegar ao ponto em que fará a inserção ou exclusão. Obviamente, o mesmo se aplica se você trabalha com mais de um local, mas ainda planeja armazenar um iterador para cada local com o qual provavelmente trabalhará, para que você manipule mais os locais que pode alcançar diretamente e raramente percorra a lista para obter para esses pontos.

Para um exemplo do primeiro, considere um navegador da web. Pode manter uma lista vinculada de Tabobjetos, com cada objeto de guia representando na guia aberta no navegador. Cada guia pode conter algumas dezenas de megabytes de dados (ou mais, especialmente se algo como um vídeo estiver envolvido). Seu número típico de guias abertas pode facilmente ser inferior a uma dúzia e 100 provavelmente está próximo do extremo superior.

Por exemplo, considere um processador de texto que armazena texto como uma lista vinculada de capítulos, cada um dos quais pode conter uma lista vinculada de (digamos) parágrafos. Quando o usuário está editando, ele normalmente encontra um local específico onde deseja editar e, em seguida, realiza uma quantidade considerável de trabalho nesse local (ou dentro desse parágrafo, de qualquer maneira). Sim, eles passarão de um parágrafo para outro de vez em quando, mas na maioria dos casos, haverá um parágrafo próximo ao local em que eles já estavam trabalhando.

De vez em quando (coisas como pesquisa global e substituição), você acaba percorrendo todos os itens de todas as listas, mas é bastante incomum e, mesmo quando o faz, provavelmente fará bastante trabalho de pesquisa dentro de um item em a lista, que o tempo para percorrê-la é quase inconseqüente.

Observe que, em um caso típico, é provável que isso também se encaixe no primeiro critério - um capítulo contém um número bastante pequeno de parágrafos, cada um dos quais provavelmente bastante grande (pelo menos em relação ao tamanho dos ponteiros no nó e tal). Da mesma forma, você possui um número relativamente pequeno de capítulos, cada um com vários kilobytes.

Dito isto, devo admitir que esses dois exemplos são provavelmente um pouco inventados e, embora uma lista vinculada possa funcionar perfeitamente bem para ambos, provavelmente não forneceria uma grande vantagem em ambos os casos. Nos dois casos, por exemplo, alocar espaço extra em um vetor para algumas páginas / guias da Web (vazias) ou para alguns capítulos vazios provavelmente não será um problema real.


4
+1, mas: O primeiro caso desaparece quando você usa ponteiros, que você sempre deve usar com objetos grandes. As listas vinculadas também não são adequadas para o exemplo da segunda; matrizes próprias para todas as operações quando são curtas.
quer

2
O estojo de objetos grandes não funciona. Usar um std::vectordos ponteiros será mais eficiente que todos os objetos do nó da lista vinculada.
Winston Ewert

Existem muitos usos para listas vinculadas - apenas que elas não são tão comuns quanto matrizes dinâmicas. Um cache LRU é um uso comum de uma lista vinculada.
Charles Salvia

Além disso, a std::vector<std::unique_ptr<T>>pode ser uma boa alternativa.
Deduplicator

24

Segundo o próprio Bjarne Stroustrup, vetores sempre devem ser a coleção padrão para sequências de dados. Você pode escolher a lista se desejar otimizar a inserção e exclusão de elementos, mas normalmente não deve. Os custos da lista são o uso lento da travessia e da memória.

Ele fala sobre isso nesta apresentação .

Por volta das 0:44, ele fala sobre vetores versus listas em geral.

A compacidade é importante. Os vetores são mais compactos que as listas. E padrões de uso previsíveis são muito importantes. Com os vetores, você precisa empurrar muitos elementos, mas os caches são muito, muito bons nisso. ... As listas não têm acesso aleatório. Mas quando você percorre uma lista, continua fazendo acesso aleatório. Há um nó aqui, e ele vai para esse nó, na memória. Portanto, você está acessando aleatoriamente sua memória e maximizando as falhas de cache, exatamente o oposto do que deseja.

Por volta de 1:08, ele recebe uma pergunta sobre esse problema.

O que deveríamos ver é que precisamos de uma sequência de elementos. E a sequência padrão de elementos em C ++ é o vetor. Agora, porque isso é compacto e eficiente. Implementação, mapeamento para hardware, é importante. Agora, se você deseja otimizar a inserção e a exclusão - você diz: 'bem, não quero a versão padrão de uma sequência. Quero o especializado, que é uma lista '. E se você fizer isso, deve saber o suficiente para dizer: 'Estou aceitando alguns custos e alguns problemas, como travessias lentas e mais uso de memória'.


1
você se importaria de escrever resumidamente o que é dito na apresentação ao qual você vincula "por volta das 0:44 e 1:08"?
Gnat

2
@gnat - certamente. Tentei citar as coisas que fazem sentido separadamente e que precisam do contexto dos slides.
Pete

11

O único lugar em que costumo usar listas é onde preciso apagar elementos e não invalidar iteradores. std::vectorinvalida todos os iteradores ao inserir e apagar. std::listgarante que os iteradores dos elementos existentes ainda sejam válidos após a inserção ou exclusão.


4

Além das outras respostas já fornecidas, as listas têm certos recursos que não existem em vetores (porque seriam incrivelmente caros.) As operações de emenda e mesclagem são as mais significativas. Se você costuma ter um monte de listas que precisam ser anexadas ou mescladas, uma lista provavelmente é uma boa escolha.

Mas se você não precisar executar essas operações, provavelmente não.


3

A falta de facilidade de cache / página inerente às listas vinculadas tendem a torná-las quase totalmente descartadas por muitos desenvolvedores de C ++, e com boa justificativa nesse formato padrão.

Listas vinculadas ainda podem ser maravilhosas

No entanto, as listas vinculadas podem ser maravilhosas quando apoiadas por um alocador fixo que lhes devolve a localidade espacial que eles inerentemente não possuem.

Onde eles se destacam é que podemos dividir uma lista em duas listas, por exemplo, simplesmente armazenando um novo ponteiro e manipulando um ou dois ponteiros. Podemos mover nós de uma lista para outra em tempo constante com mera manipulação de ponteiro, e uma lista vazia pode simplesmente ter o custo de memória de um único headponteiro.

Acelerador de grade simples

Como um exemplo prático, considere uma simulação visual 2D. Ele tem uma tela de rolagem com um mapa que mede 400x400 (160.000 células de grade) usado para acelerar coisas como a detecção de colisão entre milhões de partículas que se movem em cada quadro (evitamos árvores quadrúpedes aqui, pois elas realmente tendem a ter um desempenho pior com este nível de dados dinâmicos). Um monte de partículas está constantemente se movendo a cada quadro, o que significa que elas passam de residir em uma célula da grade para outra constantemente.

Nesse caso, se cada partícula for um nó de lista vinculado individualmente, cada célula da grade poderá iniciar como apenas um headponteiro que aponta para nullptr. Quando uma nova partícula nasce, apenas a colocamos na célula da grade em que reside, definindo o headponteiro dessa célula para apontar para esse nó de partícula. Quando uma partícula se move de uma célula da grade para a próxima, apenas manipulamos ponteiros.

Isso pode ser muito mais eficiente do que armazenar 160.000 vectorspara cada célula da grade e afastar e apagar do meio o tempo todo, quadro a quadro.

std :: list

Isso é para listas enroladas manualmente, intrusivas e vinculadas individualmente, suportadas por um alocador fixo. std::listrepresenta uma lista duplamente vinculada e pode não ser tão compacta quando vazia como um único ponteiro (varia de acordo com a implementação do fornecedor), além de ser uma dor de cabeça implementar alocadores personalizados no std::allocatorformulário.

Devo admitir que nunca uso listnada. Mas listas vinculadas ainda podem ser maravilhosas! No entanto, eles não são maravilhosos pelas razões pelas quais as pessoas costumam ser tentadas a usá-los, e não tão maravilhosos, a menos que sejam apoiados por um alocador fixo muito eficiente que atenua pelo menos as falhas de página obrigatórias e as falhas de cache associadas.


1
Existe uma lista vinculada individual padrão desde C ++ 11 std::forward_list,.
sharyex

2

Você deve considerar o tamanho dos elementos no contêiner.

int O vetor de elementos é muito rápido, pois a maioria dos dados se encaixa no cache da CPU (e as instruções SIMD provavelmente podem ser usadas para a cópia de dados).

Se o tamanho do elemento for maior, o resultado dos testes 1 e 3 poderá mudar significativamente.

A partir de uma comparação de desempenho muito abrangente :

Isso tira conclusões simples sobre o uso de cada estrutura de dados:

  • Trituração de números: use std::vectoroustd::deque
  • Pesquisa linear: use std::vectoroustd::deque
  • Inserção / remoção aleatória:
    • Tamanho pequeno dos dados: use std::vector
    • Tamanho do elemento grande: use std::list(a menos que se destine principalmente à pesquisa)
  • Tipo de dados não trivial: use a std::listmenos que você precise do contêiner, especialmente para pesquisar. Mas para várias modificações do contêiner, será muito lento.
  • Empurre para frente: use std::dequeoustd::list

(como observação lateral, std::dequeé uma estrutura de dados muito subestimada).

Do ponto de vista da conveniência, std::listgarante que os iteradores nunca sejam invalidados quando você insere e remove outros elementos. Muitas vezes, é um aspecto fundamental.


2

A razão mais proeminente para usar listas na minha opinião é a invalidação do iterador : se você adicionar / remover elementos a um vetor, todos os ponteiros, referências e iteradores que você manteve em elementos específicos desse vetor poderão ser invalidados e levar a erros sutis. falhas de segmentação.

Este não é o caso de listas.

As regras precisas para todos os contêineres padrão são fornecidas nesta postagem do StackOverflow .


0

Em suma, não há uma boa razão para usar std::list<>:

  • Se você precisar de um contêiner não classificado, std::vector<>regras.
    (Exclua os elementos substituindo-os pelo último elemento do vetor.)

  • Se você precisar de um contêiner classificado, std::vector<shared_ptr<>>regras.

  • Se você precisar de um índice esparso, std::unordered_map<>regras.

É isso aí.

Acho que há apenas uma situação em que costumo usar uma lista vinculada: Quando tenho objetos preexistentes que precisam ser conectados de alguma maneira para implementar alguma lógica de aplicativo adicional. No entanto, nesse caso, eu nunca uso std::list<>, em vez disso, recorro a um próximo ponteiro (inteligente) dentro do objeto, especialmente porque a maioria dos casos de uso resulta em uma árvore e não em uma lista linear. Em alguns casos, a estrutura resultante é uma lista vinculada, em outros, é uma árvore ou um gráfico acíclico direcionado. O objetivo principal desses ponteiros é sempre construir estrutura lógica, nunca gerenciar objetos. Nós temos std::vector<>para isso.


-1

Você precisa mostrar como estava fazendo as inserções no seu primeiro teste. Seus segundo e terceiro testes, o vetor vencerão facilmente.

Um uso significativo das listas é quando você precisa oferecer suporte à remoção de itens durante a iteração. Quando o vetor é modificado, todos os iteradores são (potencialmente) inválidos. Com uma lista, apenas um iterador para o elemento removido é inválido. Todos os outros iteradores permanecem válidos.

A ordem típica de uso dos contêineres é vector, deque e listar. A escolha do contêiner geralmente é baseada no vetor push_back, pop_front, escolha deque, insira a lista de opções.


3
ao remover itens durante a iteração, geralmente é melhor usar um vetor e apenas criar um novo vetor para os resultados
amara

-1

Um fator em que consigo pensar é que, à medida que um vetor cresce, a memória livre fica fragmentada à medida que o vetor desaloca sua memória e aloca um bloco maior repetidamente. Isso não será um problema com as listas.

Isso ocorre além do fato de que um grande número de push_backs sem reserva também causará uma cópia durante cada redimensionamento, o que a torna ineficiente. Inserir no meio da mesma forma causa um movimento de todos os elementos para a direita e é ainda pior.

Mas não sei se essa é uma grande preocupação, mas foi a razão que me foi dada no meu trabalho (desenvolvimento de jogos para dispositivos móveis), para evitar vetores.


1
não, o vetor será copiado e isso é caro. Mas percorrer a lista vinculada (para descobrir onde inserir) também é caro. A chave é realmente medir
Kate Gregory

@KateGregory eu quis dizer além de que, Deixe-me editar em conformidade
Karthik T

3
Certo, mas acredite ou não (e a maioria das pessoas não acredita) o custo que você não mencionou, percorrendo a lista vinculada para descobrir onde inserir OUTWEIGHS essas cópias (especialmente se os elementos forem pequenos (ou móveis, porque então são movimentos)) e o vetor é frequentemente (ou até mesmo geralmente) mais rápido. Acredite ou não.
Kate Gregory
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.