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?
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.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.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?
fonte
list
provavelmente faz melhor se você está removendo muitos elementos. Eu não acredito que umavector
vez 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 ondelist
é lento. A inserção real será mais rápida que o vetor.Respostas:
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::list
talvez 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
Tab
objetos, 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.
fonte
std::vector
dos ponteiros será mais eficiente que todos os objetos do nó da lista vinculada.std::vector<std::unique_ptr<T>>
pode ser uma boa alternativa.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.
Por volta de 1:08, ele recebe uma pergunta sobre esse problema.
fonte
O único lugar em que costumo usar listas é onde preciso apagar elementos e não invalidar iteradores.
std::vector
invalida todos os iteradores ao inserir e apagar.std::list
garante que os iteradores dos elementos existentes ainda sejam válidos após a inserção ou exclusão.fonte
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.
fonte
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
head
ponteiro.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
head
ponteiro que aponta paranullptr
. Quando uma nova partícula nasce, apenas a colocamos na célula da grade em que reside, definindo ohead
ponteiro 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
vectors
para 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::list
representa 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 nostd::allocator
formulário.Devo admitir que nunca uso
list
nada. 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.fonte
std::forward_list
,.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 :
(como observação lateral,
std::deque
é uma estrutura de dados muito subestimada).Do ponto de vista da conveniência,
std::list
garante que os iteradores nunca sejam invalidados quando você insere e remove outros elementos. Muitas vezes, é um aspecto fundamental.fonte
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 .
fonte
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 temosstd::vector<>
para isso.fonte
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.
fonte
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_back
s 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.
fonte