Qual contêiner STL devo usar para um FIFO?

89

Qual contêiner STL atenderia melhor às minhas necessidades? Eu basicamente tenho um contêiner de 10 elementos de largura no qual eu continuamente push_backnovos elementos enquanto pop_frontingiro o elemento mais antigo (cerca de um milhão de vezes).

No momento, estou usando um std::dequepara a tarefa, mas std::listgostaria de saber se a seria mais eficiente, já que não precisaria se realocar (ou talvez eu esteja confundindo a std::dequecom a std::vector?). Ou existe um contêiner ainda mais eficiente para minha necessidade?

PS Eu não preciso de acesso aleatório

Gab Royer
fonte
5
Por que não experimentar com ambos e cronometrar para ver qual é o mais rápido para sua necessidade?
KTC
5
Eu estava prestes a fazer isso, mas também procurava uma resposta teórica.
Gab Royer
2
o std::dequenão será realocado. É um híbrido de a std::liste a, std::vectoronde aloca pedaços maiores do que a, std::listmas não realoca como a std::vector.
Matt Price
2
Não, aqui está a garantia relevante do padrão: "Inserir um único elemento no início ou no final de um deque sempre leva um tempo constante e causa uma única chamada ao construtor de cópia de T."
Matt Price
1
@John: Não, ele aloca novamente. Talvez estejamos apenas misturando termos. Acho que realocar significa pegar a alocação antiga, copiá-la para uma nova alocação e descartar a antiga.
GManNickG

Respostas:

194

Como há uma infinidade de respostas, você pode ficar confuso, mas para resumir:

Use um std::queue. A razão para isso é simples: é uma estrutura FIFO. Você quer FIFO, você usa um std::queue.

Ele deixa sua intenção clara para qualquer outra pessoa, e até para você mesmo. A std::listou std::dequenão. Uma lista pode inserir e remover em qualquer lugar, o que não é o que uma estrutura FIFO deve fazer, e um dequepode adicionar e remover de qualquer uma das extremidades, o que também é algo que uma estrutura FIFO não pode fazer.

É por isso que você deve usar a queue.

Agora, você perguntou sobre o desempenho. Em primeiro lugar, lembre-se sempre desta importante regra prática : Bom código primeiro, desempenho por último.

A razão para isso é simples: pessoas que buscam desempenho antes da limpeza e elegância quase sempre terminam por último. O código deles se torna uma massa de mingau, porque eles abandonaram tudo o que era bom para realmente não conseguir nada com isso.

Escrevendo um código bom e legível primeiro, a maioria dos problemas de desempenho se resolverá por si mesmos. E se mais tarde você descobrir que seu desempenho está deficiente, agora é fácil adicionar um criador de perfil ao seu código bom e limpo e descobrir onde está o problema.

Dito isso, std::queueé apenas um adaptador. Ele fornece a interface segura, mas usa um contêiner diferente no interior. Você pode escolher esse contêiner subjacente e isso permite uma boa dose de flexibilidade.

Então, qual contêiner subjacente você deve usar? Sabemos que std::liste std::dequeambos fornecem as funções necessárias ( push_back(), pop_front()e front()), então como é que vamos decidir?

Em primeiro lugar, entenda que alocar (e desalocar) memória não é algo rápido de se fazer, geralmente, porque envolve ir ao SO e pedir que ele faça algo. A listprecisa alocar memória toda vez que algo é adicionado e desalocá-lo quando ele for removido.

A deque, por outro lado, aloca em blocos. Ele alocará com menos frequência do que a list. Pense nisso como uma lista, mas cada bloco de memória pode conter vários nós. (Claro, sugiro que você realmente aprenda como funciona .)

Então, só com isso um dequedeveria ter um desempenho melhor, porque não lida com a memória com tanta frequência. Combinado com o fato de você estar manipulando dados de tamanho constante, provavelmente não será necessário alocar após a primeira passagem pelos dados, enquanto uma lista estará constantemente alocando e desalocando.

Uma segunda coisa a entender é o desempenho do cache . Ir para a RAM é lento, então quando a CPU realmente precisa, ela tira o melhor proveito desse tempo, levando um pedaço de memória de volta para o cache. Como um dequealoca em pedaços de memória, é provável que acessar um elemento neste contêiner fará com que a CPU traga de volta o resto do contêiner também. Agora, qualquer outro acesso ao dequeserá rápido, porque os dados estão no cache.

Isso é diferente de uma lista, onde os dados são alocados um de cada vez. Isso significa que os dados podem ser espalhados por todo o lugar na memória, e o desempenho do cache será ruim.

Portanto, considerando isso, a dequedeve ser uma escolha melhor. É por isso que é o contêiner padrão ao usar um queue. Dito isso, este ainda é apenas um palpite (muito) educado: você terá que traçar o perfil deste código, usando um dequeem um teste e listno outro para realmente saber com certeza.

Mas lembre-se: faça o código funcionar com uma interface limpa e se preocupe com o desempenho.

John levanta a preocupação de que envolver um listou dequecausará uma diminuição no desempenho. Mais uma vez, ele e eu podemos dizer com certeza sem criar o perfil, mas as chances são de que o compilador irá embutir as chamadas que ele queuefaz. Ou seja, quando você diz queue.push(), ele realmente apenas diz queue.container.push_back(), ignorando completamente a chamada de função.

Mais uma vez, essa é apenas uma suposição educada, mas usar um queuenão prejudicará o desempenho, quando comparado ao uso do contêiner básico bruto. Como eu disse antes, use o queue, porque é limpo, fácil de usar e seguro, e se realmente se torna um problema, perfil e teste.

GManNickG
fonte
10
+1 - e se o boost :: circular_buffer <> tiver o melhor desempenho, então apenas use-o como o contêiner subjacente (ele também fornece o push_back (), pop_front (), front () e back () necessários )
Michael Burr
2
Aceito por explicar em detalhes (que é o que eu precisava, obrigado pelo tempo). Quanto ao bom código primeiro desempenho por último, tenho que admitir que é um dos meus maiores padrões, sempre tento fazer as coisas perfeitamente na primeira execução ... Eu escrevi o código usando um deque primeiro difícil, mas como a coisa não era t tendo um desempenho tão bom quanto pensei (é para ser quase em tempo real), achei que deveria melhorá-lo um pouco. Como Neil também disse, eu realmente deveria ter usado um profiler ... Embora eu esteja feliz por ter cometido esses erros agora, embora isso realmente não importe. Muito obrigado a todos vocês.
Gab Royer
4
-1 por não resolver o problema e resposta inútil inchada. A resposta certa aqui é curta e é boost :: circular_buffer <>.
Dmitry Chichkov
1
"Bom código primeiro, desempenho por último", é uma frase incrível. Se ao menos todos entendessem isso :)
thegreendroid
Agradeço a ênfase na criação de perfis. Fornecer uma regra
prática
28

Confira std::queue. Ele envolve um tipo de contêiner subjacente e o contêiner padrão é std::deque.

Mark Ransom
fonte
3
Cada camada extra será eliminada pelo compilador. Pela sua lógica, todos nós deveríamos apenas programar em assembly, já que a linguagem é apenas um shell que atrapalha. O objetivo é usar o tipo correto para o trabalho. E queueé esse tipo. Bom código primeiro, desempenho depois. Inferno, a maior parte do desempenho vem do uso de um bom código em primeiro lugar.
GManNickG
2
Desculpe ser vago - meu ponto é que uma fila é exatamente o que a pergunta estava pedindo, e os designers de C ++ pensaram que deque era um bom contêiner subjacente para este caso de uso.
Mark Ransom
2
Não há nada nesta pergunta que indique que ele encontrou falta de desempenho. Muitos iniciantes estão constantemente perguntando sobre a solução mais eficiente para qualquer problema, independentemente de a solução atual ter um desempenho aceitável ou não.
jalf
1
@John, se ele achasse que o desempenho estava deficiente, retirar a proteção que queuefornece a segurança não aumentaria o desempenho, como venho dizendo. Você sugeriu um list, que provavelmente terá um desempenho pior.
GManNickG
3
O que acontece com std :: queue <> é que se deque <> não é o que você deseja (para desempenho ou qualquer outra razão), é uma linha única mudá-lo para usar um std :: list como armazenamento de apoio - como GMan disse lá atrás. E se você realmente deseja usar um buffer de anel em vez de uma lista, boost :: circular_buffer <> aparecerá diretamente em ... std :: queue <> é quase definitivamente a 'interface' que deve ser usada. A loja de apoio para ele pode ser alterada praticamente à vontade.
Michael Burr
7

Eu continuamente push_backnovos elementos enquanto pop_fronting o elemento mais antigo (cerca de um milhão de vezes).

Um milhão não é realmente um número grande na computação. Como outros sugeriram, use a std::queuecomo sua primeira solução. No caso improvável de ser muito lento, identifique o gargalo usando um criador de perfil (não adivinhe!) E reimplemente usando um contêiner diferente com a mesma interface.

Fénix
fonte
1
Bem, é um número grande, pois o que eu quero fazer deve ser em tempo real. Embora você esteja certo de que eu deveria ter usado um criador de perfil para identificar a causa ...
Gab Royer
Acontece que não estou acostumado a usar um criador de perfil (usamos gprof um pouco em uma de nossas aulas, mas realmente não fomos a fundo ...). Se você pudesse me indicar alguns recursos, eu agradeceria muito! PS. Eu uso VS2008
Gab Royer
@Gab: Qual VS2008 você tem (Express, Pro ...)? Alguns vêm com um profiler.
sbi
@Gab Desculpe, não uso mais o VS, então não posso aconselhar
@Sbi, pelo que estou vendo está apenas na edição do sistema de equipes (a qual tenho acesso). Vou dar uma olhada nisso.
Gab Royer
5

Porque não std::queue? Tudo o que tem é push_backe pop_front.

eduffy
fonte
3

Uma fila é provavelmente uma interface mais simples do que um deque, mas para uma lista tão pequena, a diferença no desempenho é provavelmente insignificante.

O mesmo vale para a lista . É apenas uma questão de escolha de qual API você deseja.

lavinio
fonte
Mas eu estava me perguntando se o push_back constante estava fazendo a fila ou deque se realocar
Gab Royer
std :: queue é um wrapper em torno de outro contêiner, então uma fila envolvendo um deque seria menos eficiente do que um deque bruto.
John Millikin
1
Para 10 itens, o desempenho provavelmente será um problema tão pequeno que a "eficiência" pode ser melhor medida em tempo de programador do que em tempo de código. E as chamadas da fila para deque por quaisquer otimizações decentes do compilador seriam reduzidas a nada.
lavinio
2
@John: Gostaria que você me mostrasse um conjunto de benchmarks que demonstram essa diferença de desempenho. Não é menos eficiente do que um deque cru. Compiladores C ++ embutidos de forma muito agressiva.
jalf
3
Eu tentei : DA rápida e suja 10 elementos contêiner com 100.000.000 pop_front () & push_back () rand () números int em Release build para velocidade em VC9 dá: list (27), queue (6), deque (6), array (8) .
KTC
0

Use a std::queue, mas esteja ciente das compensações de desempenho das duas Containerclasses padrão .

Por padrão, std::queueé um adaptador na parte superior std::deque. Normalmente, isso dará um bom desempenho quando você tiver um pequeno número de filas contendo um grande número de entradas, o que é indiscutivelmente o caso comum.

No entanto, não se preocupe com a implementação de std :: deque . Especificamente:

"... deques normalmente têm um grande custo mínimo de memória; um deque contendo apenas um elemento deve alocar seu array interno completo (por exemplo, 8 vezes o tamanho do objeto em libstdc ++ de 64 bits; 16 vezes o tamanho do objeto ou 4096 bytes, o que for maior , em libc ++ de 64 bits). "

Para compensar isso, presumindo que uma entrada de fila seja algo que você gostaria de enfileirar, ou seja, de tamanho razoavelmente pequeno, então se você tiver 4 filas, cada uma contendo 30.000 entradas, a std::dequeimplementação será a opção de escolha. Por outro lado, se você tiver 30.000 filas, cada uma contendo 4 entradas, é mais do que provável que a std::listimplementação seja ótima, pois você nunca amortizará a std::dequesobrecarga nesse cenário.

Você lerá muitas opiniões sobre como o cache é rei, como Stroustrup odeia listas vinculadas, etc., e tudo isso é verdade, sob certas condições. Apenas não aceite com fé cega, porque em nosso segundo cenário, é bastante improvável que a std::dequeimplementação padrão funcione. Avalie seu uso e meça.

Allan Bazinet
fonte
-1

Este caso é simples o suficiente para que você possa escrever o seu próprio. Aqui está algo que funciona bem para situações de microcontrolador em que o uso de STL ocupa muito espaço. É uma boa maneira de passar dados e sinais do manipulador de interrupções para o loop principal.

// FIFO with circular buffer
#define fifo_size 4

class Fifo {
  uint8_t buff[fifo_size];
  int writePtr = 0;
  int readPtr = 0;
  
public:  
  void put(uint8_t val) {
    buff[writePtr%fifo_size] = val;
    writePtr++;
  }
  uint8_t get() {
    uint8_t val = NULL;
    if(readPtr < writePtr) {
      val = buff[readPtr%fifo_size];
      readPtr++;
      
      // reset pointers to avoid overflow
      if(readPtr > fifo_size) {
        writePtr = writePtr%fifo_size;
        readPtr = readPtr%fifo_size;
      }
    }
    return val;
  }
  int count() { return (writePtr - readPtr);}
};
user10658782
fonte
Mas como / quando isso aconteceria?
user10658782
Oh, eu pensei que poderia por algum motivo. Deixa pra lá!
Ry-