¿Qué contenedor STL debo usar para un FIFO?

93

¿Qué contenedor STL se adaptaría mejor a mis necesidades? Básicamente tengo un contenedor de 10 elementos de ancho en el que continuamentepush_back nuevos elementos mientras pop_frontingiero el elemento más antiguo (aproximadamente un millón de veces).

Actualmente estoy usando a std::dequepara la tarea, pero me preguntaba si std::listsería más eficiente ya que no necesitaría reasignarme (¿o tal vez estoy confundiendo a std::dequecon a std::vector?). ¿O hay un contenedor aún más eficiente para mis necesidades?

PD: no necesito acceso aleatorio

Gab Royer
fuente
5
¿Por qué no probarlo con ambos y cronometrarlo para ver cuál es más rápido para sus necesidades?
KTC
5
Estaba a punto de hacer esto, pero también estaba buscando una respuesta teórica.
Gab Royer
2
el std::dequeno va a reasignar. Es un híbrido de a std::listy a std::vectordonde asigna partes más grandes que a, std::listpero no se reasignan como a std::vector.
Matt Price
2
No, aquí está la garantía relevante del estándar: "Insertar un solo elemento al principio o al final de una deque siempre lleva un tiempo constante y provoca una única llamada al constructor de copia de T."
Matt Price
1
@John: No, se asigna de nuevo. Quizás solo estamos mezclando términos. Creo que reasignar significa tomar la asignación anterior, copiarla en una nueva asignación y descartar la anterior.
GManNickG

Respuestas:

198

Dado que hay una gran cantidad de respuestas, es posible que se sienta confundido, pero para resumir:

Utilice un std::queue. La razón de esto es simple: es una estructura FIFO. Si quieres FIFO, usas un std::queue.

Deja en claro su intención para los demás, e incluso para usted mismo. A std::listo std::dequeno. Una lista puede insertar y eliminar en cualquier lugar, que no es lo que se supone que debe hacer una estructura FIFO, y dequepuede agregar y eliminar desde cualquier extremo, que también es algo que una estructura FIFO no puede hacer.

Es por eso que debe usar un queue.

Ahora, preguntaste sobre el rendimiento. En primer lugar, recuerde siempre esta importante regla general: el buen código primero, el rendimiento al final.

La razón de esto es simple: las personas que se esfuerzan por el rendimiento antes de la limpieza y la elegancia casi siempre terminan en último lugar. Su código se convierte en una papilla de mierda, porque han abandonado todo lo que es bueno para realmente no sacar nada de él.

Al escribir primero un código bueno y legible, la mayoría de los problemas de rendimiento se resolverán por sí mismos. Y si más tarde descubre que su rendimiento es deficiente, ahora es fácil agregar un generador de perfiles a su código agradable y limpio y averiguar dónde está el problema.

Dicho todo esto, std::queuees solo un adaptador. Proporciona la interfaz segura, pero utiliza un contenedor diferente en el interior. Puede elegir este contenedor subyacente y esto permite una gran flexibilidad.

Entonces, ¿qué contenedor subyacente debería usar? Lo sabemos std::listystd::deque ambos proporcionan las funciones necesarias ( push_back(), pop_front()yfront() ), por lo que ¿cómo decidir?

Primero, comprenda que asignar (y desasignar) memoria no es algo rápido, generalmente, porque implica ir al sistema operativo y pedirle que haga algo. A listtiene que asignar memoria cada vez que se agrega algo y desasignarlo cuando desaparece.

A deque, por otro lado, asigna en trozos. Se asignará con menos frecuencia que a list. Piense en ello como una lista, pero cada fragmento de memoria puede contener varios nodos. (Por supuesto, le sugiero que realmente aprenda cómo funciona ).

Entonces, con eso solo un deque debería funcionar mejor, porque no se ocupa de la memoria con tanta frecuencia. Combinado con el hecho de que está manejando datos de tamaño constante, probablemente no tendrá que asignar después del primer paso a través de los datos, mientras que una lista se asignará y desasignará constantemente.

Una segunda cosa a comprender es el rendimiento de la caché . Salir a la RAM es lento, por lo que cuando la CPU realmente lo necesita, aprovecha al máximo este tiempo llevándose una parte de la memoria a la caché. Debido a que dequeasigna fragmentos de memoria, es probable que acceder a un elemento en este contenedor haga que la CPU recupere el resto del contenedor también. Ahora cualquier otro acceso a ladeque será más rápido, porque los datos están en la caché.

Esto es diferente a una lista, donde los datos se asignan uno a la vez. Esto significa que los datos podrían estar esparcidos por todas partes en la memoria y el rendimiento de la caché será malo.

Entonces, considerando eso, dequedebería ser una mejor opción. Es por eso que es el contenedor predeterminado cuando se usa un queue. Dicho todo esto, esto sigue siendo solo una suposición (muy) educada: tendrá que perfilar este código, utilizando una dequeprueba en una ylist en la otra para saberlo con certeza.

Pero recuerde: haga que el código funcione con una interfaz limpia, luego preocúpese por el rendimiento.

John plantea la preocupación de que envolver un listo dequeprovocará una disminución del rendimiento. Una vez más, ni él ni yo podemos decirlo con certeza sin perfilarlo nosotros mismos, pero es probable que el compilador incorpore las llamadas que queuerealiza. Es decir, cuando dices queue.push(), realmente solo diráqueue.container.push_back() , omitiendo la llamada a la función por completo.

Una vez más, esto es solo una suposición queuefundamentada , pero el uso de a no degradará el rendimiento, en comparación con el uso del contenedor subyacente sin procesar. Como dije antes, use el queue, porque es limpio, fácil de usar y seguro, y si realmente se convierte en un perfil de problema y prueba.

GManNickG
fuente
10
+1, y si resulta que boost :: circular_buffer <> tiene el mejor rendimiento, utilícelo como el contenedor subyacente (también proporciona el push_back (), pop_front (), front () y back () requeridos ).
Michael Burr
2
Aceptado por explicarlo en detalle (que es lo que necesitaba, gracias por tomarte el tiempo). En cuanto al buen código, el primer rendimiento es el último, debo admitir que es uno de mis mayores valores predeterminados, siempre trato de hacer las cosas perfectamente en la primera ejecución ... escribí el código usando un deque primero difícil, pero dado que la cosa no fue ' Si funciona tan bien como pensaba (se supone que es casi en tiempo real), supuse que debería mejorarlo un poco. Como también dijo Neil, debería haber usado un generador de perfiles ... Aunque estoy feliz de haber cometido estos errores ahora, aunque en realidad no importa. Muchas gracias a todos.
Gab Royer
4
-1 por no resolver el problema y respuesta inútil hinchada. La respuesta correcta aquí es corta y es boost :: circular_buffer <>.
Dmitry Chichkov
1
"Buen código primero, rendimiento al final", es una cita increíble. Si todo el mundo entendiera esto :)
thegreendroid
Aprecio el énfasis en la elaboración de perfiles. Proporcionar una regla general es una cosa y luego probarla con la creación de perfiles es una cosa mejor
talekeDskobeDa
28

Revisa std::queue . Envuelve un tipo de contenedor subyacente, y el contenedor predeterminado es std::deque.

Mark Ransom
fuente
3
Cada capa adicional será eliminada por el compilador. Según su lógica, todos deberíamos programar en ensamblador, ya que el lenguaje es solo un shell que se interpone en el camino. El punto es utilizar el tipo correcto para el trabajo. Y queuees de ese tipo. Buen código primero, rendimiento después. Demonios, la mayor parte del rendimiento se obtiene al usar un buen código en primer lugar.
GManNickG
2
Lamento ser vago, mi punto era que una cola es exactamente lo que pedía la pregunta, y los diseñadores de C ++ pensaron que deque era un buen contenedor subyacente para este caso de uso.
Mark Ransom
2
No hay nada en esta pregunta que indique que le haya faltado rendimiento. Muchos principiantes preguntan constantemente cuál es la solución más eficaz para cualquier problema dado, independientemente de si su solución actual funciona de manera aceptable o no.
jalf
1
@John, si descubrió que el rendimiento es deficiente, quitar el caparazón de la seguridad queueno aumentaría el rendimiento, como he dicho. Sugirió una list, que probablemente funcionará peor.
GManNickG
3
Lo que pasa con std :: queue <> es que si deque <> no es lo que quiere (por rendimiento o por cualquier motivo), es una línea simple cambiarlo para usar un std :: list como la tienda de respaldo, como GMan dijo camino de regreso. Y si realmente desea usar un búfer de anillo en lugar de una lista, boost :: circular_buffer <> aparecerá justo en ... std :: queue <> es casi definitivamente la 'interfaz' que debería usarse. La tienda de respaldo se puede cambiar prácticamente a voluntad.
Michael Burr
7

Yo continuamente push_backnuevos elementos mientras tomo pop_frontel elemento más antiguo (alrededor de un millón de veces).

Un millón no es realmente un gran número en informática. Como han sugerido otros, utilice a std::queuecomo primera solución. En el improbable caso de que sea demasiado lento, identifique el cuello de botella usando un generador de perfiles (¡no adivine!) Y vuelva a implementarlo usando un contenedor diferente con la misma interfaz.

fénix
fuente
1
Bueno, la cosa es que es un gran número, ya que se supone que lo que quiero hacer es en tiempo real. Aunque tiene razón en que debería haber usado un generador de perfiles para identificar la causa ...
Gab Royer
La cuestión es que no estoy acostumbrado a usar un generador de perfiles (hemos usado un poco gprof en una de nuestras clases, pero realmente no profundizamos ...). Si pudiera indicarme algunos recursos, se lo agradecería enormemente. PD. Yo uso VS2008
Gab Royer
@Gab: ¿Qué VS2008 tienes (Express, Pro ...)? Algunos vienen con un perfilador.
sbi
@Gab Lo siento, ya no uso VS, así que no puedo aconsejarlo
@Sbi, por lo que estoy viendo, solo está en la edición del sistema de equipo (a la que tengo acceso). Voy a investigar esto.
Gab Royer
5

¿Por qué no std::queue? Todo lo que tiene es push_backy pop_front.

eduffy
fuente
3

Una cola es probablemente una interfaz más simple que una deque, pero para una lista tan pequeña, la diferencia de rendimiento probablemente sea insignificante.

Lo mismo ocurre con la lista . Depende de la elección de la API que desee.

lavinio
fuente
Pero me preguntaba si el retroceso constante estaba haciendo que la cola o la deque se reasignaran a sí mismos
Gab Royer
std :: queue es un envoltorio alrededor de otro contenedor, por lo que una cola envolviendo un deque sería menos eficiente que un deque sin formato.
John Millikin
1
Para 10 elementos, es muy probable que el rendimiento sea un problema tan pequeño, que la "eficiencia" podría medirse mejor en tiempo de programación que en tiempo de código. Y las llamadas de cola a deque por cualquier optimización de compilador decente se reducirían a nada.
lavinio
2
@John: Me gustaría que me mostrara un conjunto de puntos de referencia que demuestren tal diferencia de rendimiento. No es menos eficiente que un deque crudo. Los compiladores de C ++ en línea de forma muy agresiva.
jalf
3
Lo he probado. : Contenedor de 10 elementos rápido y sucio de DA con 100,000,000 números pop_front () y push_back () rand () int en Release build para velocidad en VC9 da: list (27), queue (6), deque (6), array (8) .
KTC
0

Utilice a std::queue, pero tenga en cuenta las compensaciones de rendimiento de las dos Containerclases estándar .

De forma predeterminada, std::queuehay un adaptador encima de std::deque. Por lo general, eso dará un buen rendimiento cuando tenga una pequeña cantidad de colas que contengan una gran cantidad de entradas, lo que posiblemente sea el caso común.

Sin embargo, no sea ciego a la implementación de std :: deque . Específicamente:

"... los deques normalmente tienen un gran costo de memoria mínimo; un deque que contiene solo un elemento tiene que asignar su matriz interna completa (por ejemplo, 8 veces el tamaño del objeto en libstdc ++ de 64 bits; 16 veces el tamaño del objeto o 4096 bytes, el que sea mayor , en libc ++ de 64 bits) ".

Para compensar eso, suponiendo que una entrada de cola es algo que le gustaría poner en cola, es decir, de tamaño razonablemente pequeño, entonces si tiene 4 colas, cada una con 30.000 entradas, la std::dequeimplementación será la opción elegida. Por el contrario, si tiene 30.000 colas, cada una con 4 entradas, lo más probable es que la std::listimplementación sea óptima, ya que nunca amortizará los std::dequegastos generales en ese escenario.

Leerás muchas opiniones sobre cómo el caché es el rey, cómo Stroustrup odia las listas enlazadas, etc., y todo eso es cierto, bajo ciertas condiciones. Simplemente no lo acepte a ciegas, porque en nuestro segundo escenario, es bastante poco probable que la std::dequeimplementación predeterminada funcione. Evalúe su uso y mida.

Allan Bazinet
fuente
-1

Este caso es lo suficientemente simple como para que puedas escribir el tuyo propio. Aquí hay algo que funciona bien para situaciones de microcontroladores donde el uso de STL ocupa demasiado espacio. Es una buena forma de pasar datos y señales desde el controlador de interrupciones a su bucle 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);}
};
usuario10658782
fuente
Pero, ¿cómo / cuándo sucedería eso?
user10658782
Oh, pensé que podría por alguna razón. ¡No importa!
Ry-