Quel conteneur STL dois-je utiliser pour un FIFO?

89

Quel conteneur STL répondrait le mieux à mes besoins? J'ai essentiellement un conteneur de 10 éléments de large dans lequel je continue de push_backcréer des éléments tout en pop_frontajoutant l'élément le plus ancien (environ un million de fois).

J'utilise actuellement un std::dequepour la tâche mais je me demandais si un std::listserait plus efficace car je n'aurais pas besoin de se réallouer (ou peut-être que je prends un std::dequepour un std::vector?). Ou existe-t-il un conteneur encore plus efficace pour mon besoin?

PS je n'ai pas besoin d'un accès aléatoire

Gab Royer
la source
5
Pourquoi ne pas l'essayer avec les deux et le chronométrer pour voir lequel est le plus rapide pour votre besoin?
KTC
5
J'étais sur le point de le faire, mais je cherchais également une réponse théorique.
Gab Royer
2
le std::dequene réallouera pas. C'est un hybride de a std::listet a std::vectoroù il alloue des morceaux plus grands que a std::listmais ne se réaffectera pas comme un std::vector.
Matt Price
2
Non, voici la garantie pertinente de la norme: "L'insertion d'un seul élément au début ou à la fin d'un deque prend toujours un temps constant et provoque un seul appel au constructeur de copie de T."
Matt Price
1
@John: Non, il alloue à nouveau. Peut-être sommes-nous simplement en train de mélanger les termes. Je pense que réallouer signifie prendre l'ancienne allocation, la copier dans une nouvelle allocation et supprimer l'ancienne.
GManNickG

Réponses:

194

Puisqu'il existe une myriade de réponses, vous pourriez être confus, mais pour résumer:

Utilisez un std::queue. La raison en est simple: il s'agit d'une structure FIFO. Vous voulez FIFO, vous utilisez un fichier std::queue.

Cela rend votre intention claire à quiconque, et même à vous-même. A std::listou std::dequepas. Une liste peut insérer et supprimer n'importe où, ce qui n'est pas ce qu'une structure FIFO est censée faire, et une dequepeut ajouter et supprimer de l'une ou l'autre extrémité, ce qu'une structure FIFO ne peut pas faire.

C'est pourquoi vous devez utiliser un fichier queue.

Maintenant, vous avez posé des questions sur les performances. Tout d'abord, rappelez-vous toujours cette règle empirique importante: un bon code d'abord, les performances en dernier.

La raison en est simple: les personnes qui recherchent la performance avant la propreté et l'élégance finissent presque toujours en dernier. Leur code devient un tas de bouillie, car ils ont abandonné tout ce qui est bon pour ne rien en tirer.

En écrivant d'abord un bon code lisible, la plupart de vos problèmes de performances se résoudront d'eux-mêmes. Et si plus tard vous trouvez que vos performances font défaut, il est maintenant facile d'ajouter un profileur à votre code agréable et propre et de découvrir où se trouve le problème.

Cela dit, ce std::queuen'est qu'un adaptateur. Il fournit l'interface sûre, mais utilise un conteneur différent à l'intérieur. Vous pouvez choisir ce conteneur sous-jacent, ce qui permet une grande flexibilité.

Alors, quel conteneur sous-jacent devriez-vous utiliser? Nous savons que std::listet à la std::dequefois fournir les fonctions nécessaires ( push_back(), pop_front()et front()), alors comment nous décidons?

Tout d'abord, comprenez que l'allocation (et la désallocation) de la mémoire n'est pas une chose rapide à faire, en général, car cela implique d'aller au système d'exploitation et de lui demander de faire quelque chose. A listdoit allouer de la mémoire à chaque fois que quelque chose est ajouté, et la désallouer quand elle disparaît.

A deque, d'autre part, alloue par blocs. Il allouera moins souvent qu'un fichier list. Considérez-le comme une liste, mais chaque bloc de mémoire peut contenir plusieurs nœuds. (Bien sûr, je vous suggère d' apprendre vraiment comment cela fonctionne .)

Donc, avec cela seul, un dequedevrait mieux fonctionner, car il ne traite pas aussi souvent de la mémoire. Mélangé au fait que vous manipulez des données de taille constante, il ne sera probablement pas nécessaire d'allouer après le premier passage des données, alors qu'une liste sera constamment allouée et désallouée.

Une deuxième chose à comprendre est la performance du cache . La sortie vers la RAM est lente, donc lorsque le processeur en a vraiment besoin, il tire le meilleur parti de ce temps en ramenant une partie de la mémoire avec lui, dans le cache. Étant donné qu'une dequeallocation dans des blocs de mémoire, il est probable que l'accès à un élément de ce conteneur amènera le processeur à ramener également le reste du conteneur. Désormais, tout accès supplémentaire au dequesera rapide, car les données sont en cache.

Cela ne ressemble pas à une liste, où les données sont allouées une par une. Cela signifie que les données peuvent être réparties partout dans la mémoire et que les performances du cache seront mauvaises.

Donc, compte tenu de cela, un dequedevrait être un meilleur choix. C'est pourquoi il s'agit du conteneur par défaut lors de l'utilisation d'un fichier queue. Cela dit, ce n'est encore qu'une supposition (très) éclairée: vous devrez profiler ce code, en utilisant un dequedans un test et listdans l'autre pour vraiment le savoir avec certitude.

Mais rappelez-vous: faites fonctionner le code avec une interface propre, puis préoccupez-vous des performances.

John soulève la crainte que l'encapsulation d'un listou dequeentraîne une diminution des performances. Encore une fois, lui ni moi ne pouvons le dire avec certitude sans le profiler nous-mêmes, mais il y a de fortes chances que le compilateur intègre les appels qu'il queueeffectue. C'est-à-dire que lorsque vous dites queue.push(), cela va simplement dire queue.container.push_back(), sauter complètement l'appel de fonction.

Encore une fois, il ne s'agit que d'une supposition éclairée, mais l'utilisation de a queuene dégradera pas les performances par rapport à l'utilisation du conteneur sous-jacent brut. Comme je l'ai déjà dit, utilisez le queue, car il est propre, facile à utiliser et sûr, et s'il devient vraiment un profil de problème et un test.

GManNickG
la source
10
+1 - et s'il s'avère que boost :: circular_buffer <> a les meilleures performances, alors utilisez-le simplement comme conteneur sous-jacent (il fournit également les push_back (), pop_front (), front () et back () requis ).
Michael Burr
2
Accepté pour l'expliquer en détail (ce dont j'avais besoin, merci d'avoir pris le temps). En ce qui concerne le bon code, la première performance en dernier, je dois admettre que c'est l'un de mes plus gros défauts, j'essaie toujours de faire les choses parfaitement lors de la première exécution ... J'ai écrit le code en utilisant un deque d'abord difficile, mais puisque la chose n'était pas. t performant aussi bien que je le pensais (il est censé être presque en temps réel), j'ai pensé que je devrais l'améliorer un peu. Comme Neil l'a également dit, j'aurais vraiment dû utiliser un profileur ... Bien que je sois heureux d'avoir commis ces erreurs maintenant, alors que cela n'a pas vraiment d'importance. Merci beaucoup à vous tous.
Gab Royer
4
-1 pour ne pas résoudre le problème et réponse inutile gonflée. La bonne réponse ici est courte et c'est boost :: circular_buffer <>.
Dmitry Chichkov
1
"Bon code d'abord, performances en dernier", c'est une citation géniale. Si seulement tout le monde l'a compris :)
thegreendroid
J'apprécie l'accent mis sur le profilage. Fournir une règle empirique est une chose, puis la prouver avec le profilage est une meilleure chose
talekeDskobeDa
28

Vérifiez std::queue. Il encapsule un type de conteneur sous-jacent et le conteneur par défaut est std::deque.

Mark Ransom
la source
3
Chaque couche supplémentaire sera éliminée par le compilateur. Selon votre logique, nous devrions tous simplement programmer en assembleur, car le langage n'est qu'un shell qui gêne. Le but est d'utiliser le type correct pour le travail. Et queueest ce type. Bon code d'abord, performances plus tard. Enfer, la plupart des performances proviennent de l'utilisation d'un bon code en premier lieu.
GManNickG
2
Désolé d'être vague - mon point était qu'une file d'attente est exactement ce que la question demandait, et les concepteurs C ++ ont pensé que deque était un bon conteneur sous-jacent pour ce cas d'utilisation.
Mark Ransom
2
Rien dans cette question n'indique qu'il a trouvé les performances insuffisantes. De nombreux débutants se demandent constamment la solution la plus efficace à un problème donné, que leur solution actuelle fonctionne de manière acceptable ou non.
jalf
1
@John, s'il trouvait les performances insuffisantes, supprimer la coque de sécurité queuen'augmenterait pas les performances, comme je l'ai dit. Vous avez suggéré un list, qui fonctionnera probablement moins bien.
GManNickG
3
Le truc à propos de std :: queue <> est que si deque <> n'est pas ce que vous voulez (pour la performance ou pour une raison quelconque), c'est un one-liner pour le changer pour utiliser un std :: list comme magasin de sauvegarde - comme Dit GMan en arrière. Et si vous voulez vraiment utiliser un tampon en anneau au lieu d'une liste, boost :: circular_buffer <> tombera directement dans ... std :: queue <> est presque certainement l '«interface» qui devrait être utilisée. Le magasin de support pour cela peut être changé à peu près à volonté.
Michael Burr
7

J'ai continuellement de push_backnouveaux éléments en pop_frontingérant l'élément le plus ancien (environ un million de fois).

Un million n'est pas vraiment un grand nombre en informatique. Comme d'autres l'ont suggéré, utilisez a std::queuecomme première solution. Dans le cas peu probable où il serait trop lent, identifiez le goulot d'étranglement à l'aide d'un profileur (ne le devinez pas!) Et ré-implémentez en utilisant un conteneur différent avec la même interface.

phénix
la source
1
Eh bien, c'est un grand nombre, car ce que je veux faire est censé être en temps réel. Bien que vous ayez raison de dire que j'aurais dû utiliser un profileur pour identifier la cause ...
Gab Royer
Le truc, c'est que je ne suis pas vraiment habitué à utiliser un profileur (nous avons un peu utilisé gprof dans l'une de nos classes mais nous ne sommes pas vraiment allés en profondeur ...). Si vous pouviez m'indiquer quelques ressources, j'apprécierais beaucoup! PS. J'utilise VS2008
Gab Royer
@Gab: Quel VS2008 avez-vous (Express, Pro ...)? Certains viennent avec un profileur.
sbi
@Gab Désolé, je ne l' utilise VS plus ne peut donc pas vraiment conseiller
@Sbi, d'après ce que je vois, c'est uniquement dans l'édition du système d'équipe (à laquelle j'ai accès). Je vais regarder ça.
Gab Royer
5

Pourquoi pas std::queue? Tout ce qu'il a, c'est push_backet pop_front.

Eduffy
la source
3

Une file d'attente est probablement une interface plus simple qu'un deque mais pour une si petite liste, la différence de performances est probablement négligeable.

Il en va de même pour la liste . C'est juste un choix de l'API que vous voulez.

Lavinio
la source
Mais je me demandais si le push_back constant faisait la file d'attente ou se réallouait
Gab Royer
std :: queue est un wrapper autour d'un autre conteneur, donc une file d'attente encapsulant un deque serait moins efficace qu'un deque brut.
John Millikin
1
Pour 10 éléments, les performances seront probablement un si petit problème, que «l'efficacité» pourrait être mieux mesurée en temps de programmeur qu'en temps de code. Et les appels de file d'attente à deque par toute optimisation décente du compilateur seraient réduits à néant.
lavinio
2
@John: J'aimerais que vous me montriez un ensemble de points de repère démontrant une telle différence de performance. Ce n'est pas moins efficace qu'un deque cru. Les compilateurs C ++ en ligne de manière très agressive.
jalf
3
J'ai essayé. : Le conteneur DA quick & dirty de 10 éléments avec 100000000 pop_front () & push_back () rand () int nombres sur Release build for speed sur VC9 donne: list (27), queue (6), deque (6), array (8) .
KTC
0

Utilisez a std::queue, mais soyez conscient des compromis de performance des deux Containerclasses standard .

Par défaut, std::queueest un adaptateur au-dessus de std::deque. En règle générale, cela donnera de bonnes performances lorsque vous avez un petit nombre de files d'attente contenant un grand nombre d'entrées, ce qui est sans doute le cas courant.

Cependant, ne soyez pas aveugle à l'implémentation de std :: deque . Plus précisément:

"... les deques ont généralement un coût mémoire minimal élevé; un deque contenant un seul élément doit allouer son tableau interne complet (par exemple, 8 fois la taille de l'objet sur libstdc ++ 64 bits; 16 fois la taille de l'objet ou 4096 octets, selon la valeur la plus grande , sur la libc ++ 64 bits). "

Pour résumer cela, en supposant qu'une entrée de file d'attente est quelque chose que vous voudriez mettre en file d'attente, c'est-à-dire, d'une taille raisonnablement petite, alors si vous avez 4 files d'attente, chacune contenant 30 000 entrées, l' std::dequeimplémentation sera l'option de choix. Inversement, si vous avez 30 000 files d'attente, chacune contenant 4 entrées, il est fort probable que l' std::listimplémentation sera optimale, car vous n'amortirez jamais les std::dequefrais généraux dans ce scénario.

Vous lirez beaucoup d'opinions sur la façon dont le cache est roi, comment Stroustrup déteste les listes liées, etc., et tout cela est vrai, sous certaines conditions. Ne l'acceptez pas sur une foi aveugle, car dans notre deuxième scénario, il est peu probable que l' std::dequeimplémentation par défaut fonctionne. Évaluez votre utilisation et mesurez.

Allan Bazinet
la source
-1

Ce cas est suffisamment simple pour que vous puissiez simplement écrire le vôtre. Voici quelque chose qui fonctionne bien pour les situations de micro-contrôleur où l'utilisation de STL prend trop de place. C'est un bon moyen de transmettre les données et le signal du gestionnaire d'interruption à votre boucle 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);}
};
user10658782
la source
Mais comment / quand cela arriverait-il?
user10658782
Oh, je pensais que c'était possible pour une raison quelconque. Ça ne fait rien!
Ry-