Какой контейнер STL мне следует использовать для FIFO?

93

Какой контейнер STL лучше всего подходит для моих нужд? По сути, у меня есть контейнер шириной 10 элементов, в котором я постоянно добавляю push_backновые элементы, pop_frontдобавляя самый старый (примерно миллион раз).

В настоящее время я использую a std::dequeдля этой задачи, но мне было интересно, будет ли a std::listболее эффективным, поскольку мне не нужно было бы перераспределять себя (или, может быть, я ошибочно принимаю a std::dequeза a std::vector?). Или есть еще более эффективный контейнер для моих нужд?

PS произвольный доступ не нужен

Габ Ройер
источник
5
Почему бы не попробовать и то, и другое, и время, чтобы увидеть, какой из них быстрее соответствует вашим потребностям?
KTC
5
Я собирался сделать это, но тоже искал теоретический ответ.
Габ Ройер,
2
std::dequeне будет перераспределить. Это гибрид a std::listи a, std::vectorгде он выделяет блоки большего размера, чем a, std::listно не перераспределяется, как a std::vector.
Мэтт Прайс,
2
Нет, это соответствующая гарантия стандарта: «Вставка одного элемента в начало или конец двухсторонней очереди всегда занимает постоянное время и вызывает единственный вызов конструктора копирования T.»
Мэтт Прайс,
1
@John: Нет, он снова распределяет. Может, мы просто путаем термины. Я думаю, что перераспределение означает взять старое выделение, скопировать его в новое и отбросить старое.
GManNickG

Ответы:

198

Поскольку существует множество ответов, вы можете запутаться, но резюмируем:

Используйте файл std::queue. Причина этого проста: это структура FIFO. Вам нужен FIFO, вы используете std::queue.

Это делает ваши намерения понятными для всех и даже для вас самих. А std::listили std::dequeнет. Список можно вставлять и удалять где угодно, что не является тем, что предполагается в структуре FIFO, и dequeможно добавлять и удалять с любого конца, что также не может сделать структура FIFO.

Вот почему вам следует использовать queue.

Теперь вы спросили о производительности. Во-первых, всегда помните это важное практическое правило: сначала хороший код, а потом - производительность.

Причина этого проста: люди, которые стремятся к производительности, прежде чем к чистоте и элегантности, почти всегда финишируют последними. Их код превращается в помойную кашу, потому что они отказались от всего хорошего, чтобы по-настоящему ничего не получить.

Если сначала написать хороший, читаемый код, большинство проблем с производительностью решатся сами собой. И если позже вы обнаружите, что ваша производительность недостаточна, теперь легко добавить профилировщик в ваш красивый, чистый код и выяснить, в чем проблема.

Тем не менее, std::queueэто всего лишь адаптер. Он обеспечивает безопасный интерфейс, но использует другой контейнер внутри. Вы можете выбрать этот базовый контейнер, и это дает большую гибкость.

Итак, какой базовый контейнер вы должны использовать? Мы знаем , что std::listи std::dequeкак обеспечить необходимые функции ( push_back(), pop_front()и front()), так как мы решили?

Во-первых, поймите, что выделение (и освобождение) памяти - это не быстрое дело, как правило, потому что для этого нужно обратиться к ОС и попросить ее что-то сделать. A listдолжен выделять память каждый раз, когда что-то добавляется, и освобождать ее, когда она исчезает.

A deque, с другой стороны, выделяет блоки. Он будет выделять реже, чем файл list. Думайте об этом как о списке, но каждый блок памяти может содержать несколько узлов. (Конечно, я бы посоветовал вам действительно узнать, как это работает .)

Таким образом, только с этим он dequeдолжен работать лучше, потому что он не так часто работает с памятью. В сочетании с тем фактом, что вы обрабатываете данные постоянного размера, им, вероятно, не придется выделять после первого прохода данных, тогда как список будет постоянно выделять и освобождать.

Второе, что нужно понять, - это производительность кеша . Выход в ОЗУ происходит медленно, поэтому, когда ЦП действительно необходимо, он максимально использует это время, забирая с собой часть памяти в кеш. Поскольку a dequeвыделяется фрагментами памяти, вполне вероятно, что доступ к элементу в этом контейнере заставит ЦП также вернуть остальную часть контейнера. Теперь любые дальнейшие обращения к ним dequeбудут быстрыми, потому что данные находятся в кеше.

Это не похоже на список, в котором данные размещаются по одному. Это означает, что данные могут быть распределены по всей памяти, и производительность кеша будет плохой.

Поэтому, учитывая это, dequeлучше выбрать a. Вот почему это контейнер по умолчанию при использовании queue. Тем не менее, это все еще (очень) обоснованное предположение: вам придется профилировать этот код, используя dequeв одном тесте и listв другом, чтобы действительно знать наверняка.

Но помните: заставьте код работать с чистым интерфейсом, а затем беспокойтесь о производительности.

Джон выражает обеспокоенность тем, что упаковка listили dequeприведет к снижению производительности. Еще раз, он и я не можем сказать наверняка, не профилируя его сами, но есть вероятность, что компилятор встроит вызовы, которые queueделает. То есть, когда вы говорите queue.push(), на самом деле он просто говорит queue.container.push_back(), полностью пропуская вызов функции.

Еще раз, это только обоснованное предположение, но использование queueне приведет к снижению производительности по сравнению с использованием базового контейнера raw. Как я уже говорил, используйте queue, потому что он чистый, простой в использовании и безопасный, и если он действительно станет проблемой, профиль и тест.

GManNickG
источник
10
+1 - и если окажется, что boost :: round_buffer <> имеет лучшую производительность, просто используйте его как базовый контейнер (он также предоставляет необходимые push_back (), pop_front (), front () и back () ).
Майкл Берр,
2
Принято для подробного объяснения (это то, что мне нужно, спасибо, что уделили время). Что касается хорошей производительности кода в первую очередь в последнюю очередь, я должен признать, что это одно из моих самых больших значений по умолчанию, я всегда стараюсь делать что-то идеально при первом запуске ... Я действительно писал код, используя deque first tough, но так как это было не так. t работает так хорошо, как я думал (предполагается, что это будет почти в реальном времени), я подумал, что должен немного улучшить его. Как также сказал Нил, мне действительно следовало использовать профилировщик ... Хотя я счастлив, что сделал эти ошибки сейчас, хотя это не имеет особого значения. Большое вам всем спасибо.
Габ Ройер,
4
-1 за то, что не решил проблему и раздутый бесполезный ответ. Правильный ответ здесь короткий, и это boost :: round_buffer <>.
Дмитрий Чичков
1
«Хороший код - в первую очередь, производительность - в последнюю очередь» - это отличная цитата. Если бы все это понимали :)
thegreendroid 02
Я ценю упор на профилирование. Предоставить практическое правило - это одно, а потом доказать его с помощью профилирования - лучше всего,
TalekeDskobeDa
28

Проверить std::queue. Он обертывает базовый тип контейнера, а контейнер по умолчанию - std::deque.

Марк Рэнсом
источник
3
Каждый лишний слой будет удален компилятором. По вашей логике, мы все должны просто программировать на ассемблере, поскольку язык - это всего лишь оболочка, которая мешает. Дело в том, чтобы использовать правильный тип для работы. И queueэто тип. Сначала хороший код, потом производительность. Черт, большая часть производительности достигается в первую очередь за счет использования хорошего кода.
GManNickG
2
Извините за расплывчатость - я хотел сказать, что очередь - это именно то, о чем спрашивал вопрос, и разработчики C ++ считали deque хорошим базовым контейнером для этого варианта использования.
Марк Рэнсом,
2
В этом вопросе нет ничего, что указывало бы на недостаток производительности. Многие новички постоянно спрашивают о наиболее эффективном решении той или иной проблемы, независимо от того, работает ли их текущее решение приемлемо или нет.
jalf
1
@John, если бы он обнаружил, что производительность недостаточна, удаление защитной оболочки queueне увеличило бы производительность, как я уже говорил. Вы предложили вариант list, который, вероятно, будет работать хуже.
GManNickG
3
Суть std :: queue <> заключается в том, что если deque <> не то, что вы хотите (для производительности или по какой-либо другой причине), это однострочный способ изменить его, чтобы использовать std :: list в качестве резервного хранилища - как GMan сказал много лет назад. И если вы действительно хотите использовать кольцевой буфер вместо списка, boost :: round_buffer <> сразу же появится ... std :: queue <> почти определенно является «интерфейсом», который следует использовать. Накопитель для него можно менять практически по желанию.
Майкл Берр,
7

Я постоянно добавляю push_backновые элементы, пока pop_frontобрабатываю самый старый элемент (около миллиона раз).

Миллион - это не так уж и много для вычислений. Как предлагали другие, используйте в std::queueкачестве первого решения. В том маловероятном случае, если он будет слишком медленным, определите узкое место с помощью профилировщика (не угадайте!) И повторно реализуйте его, используя другой контейнер с тем же интерфейсом.

Феникс
источник
1
Дело в том, что это большое число, поскольку то, что я хочу делать, должно быть в режиме реального времени. Хотя вы правы, я должен был использовать профилировщик, чтобы определить причину ...
Габ Ройер,
Дело в том, что я на самом деле не привык использовать профилировщик (мы немного использовали gprof в одном из наших классов, но мы действительно не вдавались в подробности ...). Если бы вы могли указать мне на некоторые ресурсы, я был бы очень признателен! PS. Я использую VS2008
Габ Ройер,
@Gab: Какой у вас VS2008 (Express, Pro ...)? Некоторые идут с профилировщиком.
sbi
@Gab Извините, я больше не использую VS, поэтому не могу ничего посоветовать
@Sbi, судя по тому, что я вижу, это только в редакции командной системы (к которой у меня есть доступ). Я займусь этим.
Габ Ройер,
5

А почему бы и нет std::queue? Все, что у него есть, это push_backи pop_front.

Eduffy
источник
3

Очередь , вероятно , более простой интерфейс , чем дека , но для такого небольшого списка, разница в производительности, вероятно , незначительна.

То же самое и со списком . Это просто выбор того, какой API вы хотите.

Лавинио
источник
Но мне было интересно, заставлял ли постоянный push_back перераспределяться очередь или deque,
Габ Ройер,
std :: queue - это оболочка для другого контейнера, поэтому очередь, охватывающая двухстороннюю очередь, будет менее эффективной, чем необработанная двухсторонняя очередь.
Джон Милликин,
1
Для 10 элементов производительность, скорее всего, будет такой крошечной проблемой, что «эффективность» лучше будет измерять во времени программиста, чем во времени кода. И вызовы из очереди в deque при любой достойной оптимизации компилятора были бы ни к чему.
lavinio
2
@John: Я бы хотел, чтобы вы показали мне набор тестов, демонстрирующих такую ​​разницу в производительности. Он не менее эффективен, чем raw deque. Компиляторы C ++ очень агрессивно встраиваются.
jalf
3
Я пробовал. : DA быстрый и грязный контейнер из 10 элементов с 100000000 pop_front () и push_back () rand () int числа в сборке Release для скорости на VC9 дает: список (27), очередь (6), deque (6), массив (8) .
KTC
0

Используйте std::queue, но помните о компромиссах производительности двух стандартных Containerклассов.

По умолчанию std::queueэто адаптер поверх std::deque. Как правило, это дает хорошую производительность, когда у вас небольшое количество очередей, содержащих большое количество записей, что, возможно, является обычным случаем.

Однако не закрывайте глаза на реализацию std :: deque . В частности:

"... двухсторонние очереди обычно имеют большую минимальную стоимость памяти; двухсторонняя очередь, содержащая только один элемент, должна выделить весь свой внутренний массив (например, в 8 раз больше размера объекта в 64-битной libstdc ++; в 16 раз больше размера объекта или 4096 байт, в зависимости от того, что больше , на 64-битной libc ++) ".

Чтобы понять это, предположим, что запись в очереди - это то, что вы хотите поставить в очередь, то есть достаточно маленького размера, тогда, если у вас есть 4 очереди, каждая из которых содержит 30 000 записей, std::dequeреализация будет вариантом выбора. И наоборот, если у вас 30 000 очередей, каждая из которых содержит 4 записи, то, скорее всего, std::listреализация будет оптимальной, так как std::dequeв этом сценарии вы никогда не окупите накладные расходы.

Вы прочтете множество мнений о том, насколько важен кеш, как Страуструп ненавидит связанные списки и т. Д., И все это правда при определенных условиях. Только не принимайте это на веру, потому что в нашем втором сценарии очень маловероятно, что std::dequeреализация по умолчанию будет работать. Оцените свое использование и измерьте.

Аллан Базине
источник
-1

Этот случай достаточно прост, и вы можете просто написать свой собственный. Вот что-то, что хорошо работает для ситуаций с микроконтроллерами, когда использование STL занимает слишком много места. Это хороший способ передать данные и сигнал от обработчика прерывания в ваш основной цикл.

// 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
источник
Но как и когда это произойдет?
user10658782
О, я почему-то думал, что может. Ничего!
Ry-