Quale contenitore STL dovrei usare per un FIFO?


89

Quale contenitore STL si adatterebbe meglio alle mie esigenze? Fondamentalmente ho un contenitore largo 10 elementi in cui inserisco continuamente push_backnuovi elementi mentre pop_frontinserisco l'elemento più vecchio (circa un milione di volte).

Attualmente sto usando a std::dequeper l'attività, ma mi chiedevo se a std::listsarebbe più efficiente poiché non avrei bisogno di riallocare se stesso (o forse sto scambiando a std::dequeper a std::vector?). Oppure esiste un contenitore ancora più efficiente per le mie necessità?

PS Non ho bisogno di un accesso casuale


5
Perché non provarlo con entrambi e cronometrare per vedere quale è più veloce per le tue esigenze?
KTC

5
Stavo per farlo, ma cercavo anche una risposta teorica.
Gab Royer

2
il std::dequenon si riallocherà. È un ibrido di a std::liste a in std::vectorcui alloca blocchi più grandi di a std::listma non si rialloca come a std::vector.
Matt Price

2
No, ecco la relativa garanzia dello standard: "L'inserimento di un singolo elemento all'inizio o alla fine di una deque richiede sempre un tempo costante e provoca una singola chiamata al costruttore di copie di T."
Matt Price

1
@ John: No, assegna di nuovo. Forse stiamo solo confondendo i termini. Penso che riallocare significhi prendere la vecchia allocazione, copiarla in una nuova allocazione e scartare quella vecchia.
GManNickG

Risposte:


194

Poiché ci sono una miriade di risposte, potresti essere confuso, ma per riassumere:

Usa un file std::queue. Il motivo è semplice: si tratta di una struttura FIFO. Vuoi FIFO, usi un filestd::queue .

Rende chiaro il tuo intento a chiunque altro, e anche a te stesso. A std::listo std::dequeno. Un elenco può inserire e rimuovere ovunque, il che non è ciò che una struttura FIFO dovrebbe fare, e un filedeque può aggiungere e rimuovere da entrambe le estremità, cosa che anche una struttura FIFO non può fare.

Questo è il motivo per cui dovresti usare un file queue .

Ora, hai chiesto delle prestazioni. In primo luogo, ricorda sempre questa importante regola pratica: prima un buon codice, infine le prestazioni.

Il motivo è semplice: le persone che aspirano alle prestazioni prima che la pulizia e l'eleganza finiscano quasi sempre per ultime. Il loro codice diventa una poltiglia, perché hanno abbandonato tutto ciò che è buono per non ricavarne davvero nulla.

Scrivendo prima un codice valido e leggibile, la maggior parte di voi problemi di prestazioni si risolverà da sola. E se in seguito scopri che le tue prestazioni sono carenti, ora è facile aggiungere un profiler al tuo codice bello e pulito e scoprire dove si trova il problema.

Detto questo, std::queueè solo un adattatore. Fornisce l'interfaccia sicura, ma utilizza un contenitore diverso all'interno. È possibile scegliere questo contenitore sottostante e ciò consente una buona dose di flessibilità.

Quindi, quale contenitore sottostante dovresti usare? Sappiamo che std::liste std::dequeforniscono entrambi le funzioni necessarie ( push_back(), pop_front(), e front()), quindi come facciamo a decidere?

In primo luogo, capire che l'allocazione (e la deallocazione) della memoria non è una cosa rapida da fare, in generale, perché implica uscire dal sistema operativo e chiedergli di fare qualcosa. A listdeve allocare la memoria ogni volta che viene aggiunto qualcosa e rilasciarla quando scompare.

A deque, d'altra parte, alloca in blocchi. Assegnerà meno spesso di un file list. Consideralo un elenco, ma ogni pezzo di memoria può contenere più nodi. (Ovviamente, ti suggerirei di imparare davvero come funziona .)

Quindi, solo con questo a dequedovrebbe funzionare meglio, perché non ha a che fare con la memoria così spesso. Mescolato con il fatto che stai gestendo dati di dimensioni costanti, probabilmente non sarà necessario allocare dopo il primo passaggio attraverso i dati, mentre un elenco verrà costantemente allocato e deallocato.

Una seconda cosa da capire sono le prestazioni della cache . Uscire dalla RAM è lento, quindi quando la CPU ne ha davvero bisogno, sfrutta al meglio questo tempo riportando con sé un pezzo di memoria nella cache. Poiché a dequealloca in blocchi di memoria, è probabile che l'accesso a un elemento in questo contenitore indurrà la CPU a ripristinare anche il resto del contenitore. Ora ogni ulteriore accesso al dequesarà veloce, perché i dati sono nella cache.

Questo è diverso da un elenco, in cui i dati vengono allocati uno alla volta. Ciò significa che i dati potrebbero essere distribuiti ovunque nella memoria e le prestazioni della cache saranno ridotte.

Quindi, considerando questo, dequedovrebbe essere una scelta migliore. Questo è il motivo per cui è il contenitore predefinito quando si utilizza un file queue. Detto questo, questa è ancora solo un'ipotesi (molto) plausibile: dovrai profilare questo codice, usando un dequein un test e listnell'altro per sapere con certezza.

Ma ricorda: fai funzionare il codice con un'interfaccia pulita, quindi preoccupati delle prestazioni.

John solleva la preoccupazione che il wrapping di un listo dequecausi una diminuzione delle prestazioni. Ancora una volta, né lui né io possiamo dirlo con certezza senza profilarlo noi stessi, ma è probabile che il compilatore inserisca le chiamate che queueeffettua. Cioè, quando dici queue.push(), dirà solo queue.container.push_back(), saltando completamente la chiamata alla funzione.

Ancora una volta, questa è solo un'ipotesi plausibile, ma l'utilizzo di a queuenon ridurrà le prestazioni, rispetto all'utilizzo del contenitore sottostante grezzo. Come ho detto prima, usa il queue, perché è pulito, facile da usare e sicuro, e se diventa davvero un problema, profilo e prova.


10
+1 - e se si scopre che boost :: circular_buffer <> ha le migliori prestazioni, allora usalo come contenitore sottostante (fornisce anche push_back (), pop_front (), front () e back () richiesti ).
Michael Burr

2
Accettato per averlo spiegato in dettaglio (che è quello di cui avevo bisogno, grazie per il tempo dedicato). Per quanto riguarda la buona performance del primo codice per ultimo, devo ammettere che è una delle mie più grandi impostazioni predefinite, cerco sempre di fare le cose perfettamente alla prima esecuzione ... Ho scritto il codice usando una deque prima dura, ma dal momento che la cosa non era l performando bene come pensavo (dovrebbe essere quasi in tempo reale), ho immaginato che avrei dovuto migliorarlo un po '. Come ha detto anche Neil, avrei davvero dovuto usare un profiler però ... Anche se sono felice di aver commesso questi errori ora, anche se non importa. Grazie mille a tutti voi.
Gab Royer

4
-1 per non aver risolto il problema e inutile risposta gonfia. La risposta giusta qui è breve ed è boost :: circular_buffer <>.
Dmitry Chichkov

1
"Buon codice prima, prestazioni alla fine", questa è una citazione fantastica. Se solo tutti lo capissero :)
thegreendroid

Apprezzo l'accento sulla profilazione. Fornire una regola pratica è una cosa e poi dimostrarlo con la profilazione è una cosa migliore
talekeDskobeDa

28

Check-out std::queue . Avvolge un tipo di contenitore sottostante e il contenitore predefinito è std::deque.


3
Ogni livello in più verrà eliminato dal compilatore. Secondo la tua logica, dovremmo tutti programmare in assembly, poiché il linguaggio è solo una shell che si intromette. Il punto è usare il tipo corretto per il lavoro. Ed queueè quel tipo. Buon codice prima, prestazioni dopo. Diamine, la maggior parte delle prestazioni deriva dall'utilizzo di un buon codice in primo luogo.
GManNickG

2
Mi dispiace essere vago - il mio punto era che una coda è esattamente ciò che la domanda stava chiedendo, e i progettisti C ++ pensavano che deque fosse un buon contenitore sottostante per questo caso d'uso.
Mark Ransom

2
Non c'è nulla in questa domanda che indichi che ha trovato carenti le prestazioni. Molti principianti chiedono costantemente la soluzione più efficace a un dato problema, indipendentemente dal fatto che la loro soluzione attuale funzioni in modo accettabile o meno.
jalf

1
@ John, se avesse trovato le prestazioni carenti, togliere il guscio di sicurezza queuefornito non aumenterebbe le prestazioni, come ho detto. Hai suggerito uno list, che probabilmente avrà prestazioni peggiori.
GManNickG

3
La cosa su std :: queue <> è che se deque <> non è quello che vuoi (per prestazioni o qualsiasi motivo), è una battuta cambiarlo per usare un std :: list come archivio di backup - come GMan ha detto di nuovo. E se vuoi davvero usare un buffer circolare invece di una lista, boost :: circular_buffer <> scenderà proprio in ... std :: queue <> è quasi sicuramente l '"interfaccia" che dovrebbe essere usata. Il supporto per esso può essere cambiato praticamente a piacimento.
Michael Burr


7

Continuo a creare push_backnuovi elementi mentre pop_frontinserisco l'elemento più vecchio (circa un milione di volte).

Un milione non è davvero un gran numero nell'informatica. Come altri hanno suggerito, usa std::queuecome prima soluzione. Nell'improbabile caso che sia troppo lento, identificare il collo di bottiglia utilizzando un profiler (non indovinare!) E reimplementarlo utilizzando un contenitore diverso con la stessa interfaccia.


1
Beh, il fatto è che è un numero grande perché quello che voglio fare dovrebbe essere in tempo reale. Anche se hai ragione sul fatto che avrei dovuto usare un profiler per identificare la causa ...
Gab Royer

Il fatto è che non sono molto abituato a usare un profiler (abbiamo usato gprof un po 'in una delle nostre classi ma non siamo davvero andati in profondità ...). Se potessi indicarmi alcune risorse, ti sarei molto grato! PS. Io uso VS2008
Gab Royer il

@Gab: Quale VS2008 hai (Express, Pro ...)? Alcuni sono dotati di un profiler.
sbi

@ Gab Scusa, non uso più VS quindi non posso davvero consigliarlo

@Sbi, da quello che vedo, è solo nell'edizione del sistema di squadra (a cui ho accesso). Studierò questo.
Gab Royer

5

Perché no std::queue? Tutto quello che ha è push_backe pop_front.


3

Una coda è probabilmente un'interfaccia più semplice di una deque, ma per un elenco così piccolo, la differenza di prestazioni è probabilmente trascurabile.

Lo stesso vale per l' elenco . Dipende solo dalla scelta dell'API che desideri.


Ma mi chiedevo se il costante push_back stesse facendo la coda o si riallocava il deque
Gab Royer

std :: queue è un wrapper attorno a un altro contenitore, quindi una coda che avvolge un deque sarebbe meno efficiente di un deque raw.
John Millikin

1
Per 10 elementi, è molto probabile che le prestazioni rappresentino un problema così piccolo, che "l'efficienza" potrebbe essere misurata meglio in tempo di programmazione che in tempo di codice. E le chiamate da coda a deque da qualsiasi ottimizzazione del compilatore decente sarebbero ridotte a nulla.
lavinio

2
@ John: vorrei che mi mostrassi una serie di benchmark che dimostrano una tale differenza di prestazioni. Non è meno efficiente di un deque grezzo. I compilatori C ++ inline in modo molto aggressivo.
jalf

3
L'ho provato. : Contenitore DA 10 elementi rapido e sporco con 100.000.000 pop_front () e push_back () numeri rand () int sulla build di rilascio per velocità su VC9 fornisce: list (27), queue (6), deque (6), array (8) .
KTC

0

Utilizzare a std::queue, ma sii consapevole dei compromessi sulle prestazioni dei due standardContainer classi .

Per impostazione predefinita, std::queueè un adattatore in cima astd::deque . In genere, ciò darà buone prestazioni in caso di un piccolo numero di code contenenti un gran numero di voci, che è probabilmente il caso comune.

Tuttavia, non essere cieco all'implementazione di std :: deque . Nello specifico:

"... i deques tipicamente hanno un grande costo minimo di memoria; un deque che contiene solo un elemento deve allocare il suo intero array interno (es. 8 volte la dimensione dell'oggetto su libstdc ++ a 64 bit; 16 volte la dimensione dell'oggetto o 4096 byte, a seconda di quale sia più grande , su libc ++ a 64 bit). "

Per rimuoverlo, presumendo che una voce di coda sia qualcosa che vorresti mettere in coda, cioè, di dimensioni ragionevolmente piccole, se hai 4 code, ciascuna contenente 30.000 voci, l' std::dequeimplementazione sarà l'opzione di scelta. Al contrario, se hai 30.000 code, ognuna contenente 4 voci, è molto probabile che l' std::listimplementazione sia ottimale, poiché non ammortizzerai mai il std::dequesovraccarico in quello scenario.

Leggerai molte opinioni su come la cache è re, su come Stroustrup odia gli elenchi collegati, ecc., E tutto ciò è vero, a determinate condizioni. Basta non accettarlo con fede cieca, perché nel nostro secondo scenario lì, è abbastanza improbabile che l' std::dequeimplementazione predefinita funzioni. Valuta il tuo utilizzo e misura.


-1

Questo caso è abbastanza semplice che puoi semplicemente scrivere il tuo. Ecco qualcosa che funziona bene per le situazioni di microcontrollore in cui l'uso di STL occupa troppo spazio. È un bel modo per passare dati e segnali dal gestore di interrupt al loop principale.

// 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);}
};

Ma come / quando sarebbe mai successo?
user10658782

Oh, ho pensato che potesse per qualche motivo. Non importa!
Ry-
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.