Який контейнер STL слід використовувати для FIFO?


93

Який контейнер STL найкраще відповідав би моїм потребам? В основному я маю контейнер із 10 елементами ширини, в якому я постійно створюю push_backнові елементи, pop_frontпереглядаючи найстаріший елемент (приблизно мільйон разів).

В даний час я використовую a std::dequeдля цього завдання, але мені було цікаво, чи a std::listбуде більш ефективним, оскільки мені не потрібно буде перерозподіляти себе (або, можливо, я приймаю std::dequea std::vector?). Або є ще більш ефективний контейнер для моїх потреб?

PS Мені не потрібен довільний доступ


5
Чому б не спробувати це з обома і не встигнути побачити, який з них швидший для ваших потреб?
KTC

5
Я збирався це зробити, але я також шукав теоретичну відповідь.
Gab Royer

2
std::dequeНЕ буде перерозподілити. Це гібрид a std::listі a, std::vectorде він виділяє більші шматки, ніж a, std::listале не перерозподіляє, як a std::vector.
Метт Прайс

2
Ні, ось відповідна гарантія від стандарту: "Вставка одного елемента на початку або в кінці дека завжди займає постійний час і викликає один виклик конструктора копіювання Т."
Matt Price

1
@ Джон: Ні, це виділяє знову. Можливо, ми просто змішуємо терміни. Я думаю, що перерозподілити означає взяти старий розподіл, скопіювати його в новий розподіл і відкинути старий.
GManNickG

Відповіді:


198

Оскільки відповідей існує безліч, ви можете заплутатися, але підсумовуючи:

Використовуйте a std::queue. Причина цього проста: це структура FIFO. Ви хочете FIFO, ви використовуєте std::queue.

Це дає зрозуміти ваші наміри будь-кому іншому, і навіть вам самим. А std::listчи std::dequeні. Список можна вставляти та видаляти де завгодно, чого не слід робити структурі FIFO, а dequeможна додавати та видаляти з будь-якого кінця, що також не може робити структура FIFO.

Ось чому вам слід використовувати a queue.

Тепер ви запитали про продуктивність. По-перше, завжди пам’ятайте про це важливе правило: спочатку хороший код, остання продуктивність.

Причина цього проста: люди, які прагнуть до продуктивності до чистоти та елегантності, майже завжди закінчують останніми. Їх код стає нечистотою, тому що вони відмовились від усього доброго, щоб насправді нічого з цього не отримати.

Написавши спочатку хороший, читабельний код, більшість з вас проблеми з продуктивністю вирішать самі. І якщо пізніше ви виявите, що вашої продуктивності не вистачає, тепер легко додати профайлер до вашого приємного, чистого коду і з’ясувати, де проблема.

Все сказане - std::queueце лише адаптер. Він забезпечує безпечний інтерфейс, але використовує інший контейнер всередині. Ви можете вибрати цей основний контейнер, і це забезпечує значну гнучкість.

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

По-перше, зрозумійте, що виділення (і вивільнення) пам’яті, як правило, не є швидким завданням, оскільки воно передбачає вихід до ОС та прохання її щось зробити. A listповинен виділяти пам'ять кожного разу, коли щось додається, і звільняти її, коли вона зникає.

А deque, з іншого боку, розподіляє шматками. Він буде виділяти рідше, ніж a list. Подумайте про це як про список, але кожен фрагмент пам’яті може містити кілька вузлів. (Звичайно, я б запропонував вам справді дізнатися, як це працює .)

Отже, лише з цим a dequeповинен працювати ефективніше, оскільки він не так часто справляється з пам’яттю. У поєднанні з тим, що ви обробляєте дані постійного розміру, їм, ймовірно, не доведеться розподіляти їх після першого проходження через дані, тоді як список буде постійно розподіляти та звільняти.

Друге, що слід зрозуміти - це продуктивність кешу . Вихід до оперативної пам’яті відбувається повільно, тому, коли процесору справді потрібно, він робить все можливе, забираючи частину пам’яті назад у кеш. Оскільки dequeрозподіл у фрагментах пам'яті, цілком ймовірно, що доступ до елемента в цьому контейнері призведе до того, що процесор поверне назад і решту контейнера. Тепер будь-який подальший доступ до dequeбуде швидким, оскільки дані знаходяться в кеші.

Це на відміну від списку, де дані розподіляються по одному. Це означає, що дані можуть поширюватися повсюдно в пам'яті, і продуктивність кешу буде поганою.

Отже, зважаючи на це, dequeкращим вибором має бути a. Ось чому це контейнер за замовчуванням при використанні queue. З усього сказаного, це все ще лише (дуже) освічена здогадка: вам доведеться профілювати цей код, використовуючи dequeв одному тесті, а listв іншому, щоб насправді точно знати.

Але пам’ятайте: змушуйте код працювати з чистим інтерфейсом, тоді турбуйтеся про продуктивність.

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

Ще раз, це лише освічена здогадка, але використання queueволі не погіршить продуктивність у порівнянні з використанням базового контейнера raw. Як я вже говорив раніше, використовуйте queue, оскільки він чистий, простий у використанні та безпечний, і якщо він справді стає проблемним профілем та тестом.


10
+1 - і якщо виявиться, що boost :: circular_buffer <> має найкращу продуктивність, тоді просто використовуйте це як базовий контейнер (він також забезпечує необхідні push_back (), pop_front (), front () і back () ).
Michael Burr

2
Прийнято пояснювати це детально (саме це мені потрібно, дякую, що знайшли час). Що стосується першого виконання хорошого коду останнього, я повинен визнати, що це одне з моїх найбільших значень за замовчуванням, я завжди намагаюся робити щось ідеально під час першого запуску ... Я писав код, використовуючи deque перший жорсткий, але оскільки справа не була Я виступав так само добре, як я думав (це має бути майже в режимі реального часу), я здогадався, що мені слід його трохи вдосконалити. Як сказав Ніл, я справді мав би використовувати профіліст ... Хоча я радий, що зробив ці помилки зараз, хоча це насправді не має значення. Всім велике спасибі.
Gab Royer

4
-1 за невирішення проблеми та роздуту марну відповідь. Правильна відповідь тут коротка і це boost :: circular_buffer <>.
Дмитро Чичков

1
"Хороший код спочатку, продуктивність остання", це приголомшлива цитата. Якби тільки всі це розуміли :)
thegreendroid

Я ціную наголос на профілюванні. Забезпечення емпіричного правила - це одне, а потім довести це за допомогою профілювання - це краще
talekeDskobeDa

28

Перевірте std::queue. Він обгортає базовий тип контейнера, а контейнером за замовчуванням є std::deque.


3
Кожен зайвий шар буде усунений компілятором. За вашою логікою, нам усім слід просто програмувати в збірці, оскільки мова - це просто оболонка, яка заважає. Суть полягає у використанні правильного типу для роботи. І queueце такий тип. Спочатку хороший код, пізніше продуктивність. До біса, більшість продуктивності - це спочатку використання хорошого коду.
GManNickG

2
Вибачте за невизначеність - моя думка полягала в тому, що черга - це саме те, про що запитувало запитання, і дизайнери C ++ вважали, що deque є хорошим базовим контейнером для цього випадку використання.
Марк Ренсом

2
У цьому питанні немає нічого, що вказує на те, що він визнав недостатню ефективність. Багато початківців постійно запитують про найбільш ефективне рішення будь-якої даної проблеми, незалежно від того, чи працює їхнє поточне рішення прийнятно чи ні.
jalf

1
@ Джон, якщо він виявив, що продуктивності бракує, видалення оболонки безпеки, що queueзабезпечує, не збільшить продуктивність, як я вже говорив. Ви запропонували a list, який, мабуть, буде працювати гірше.
GManNickG

3
Річ у std :: queue <> полягає в тому, що якщо deque <> - це не те, що ви хочете (для виконання чи з будь-якої іншої причини), це однолінійний лайнер, щоб змінити його, щоб використовувати std :: list як резервний магазин - як GMan сказав шлях назад. І якщо ви дійсно хочете використовувати буфер кільця замість списку, boost :: circular_buffer <> потрапить прямо в ... std :: queue <> - це майже напевно «інтерфейс», який слід використовувати. Магазин підкладки для нього можна змінити майже за власним бажанням.
Michael Burr


7

Я постійно створюю push_backнові елементи, pop_frontпереглядаючи найстаріший елемент (близько мільйона разів).

Мільйон - це насправді не велика цифра в обчислювальній техніці. Як пропонували інші, використовуйте a std::queueяк перше рішення. Навряд чи це буде занадто повільно, визначте вузьке місце за допомогою профілювача (не вгадуйте!) Та повторно реалізуйте, використовуючи інший контейнер з однаковим інтерфейсом.


1
Ну справа в тому, що це велика цифра, оскільки те, що я хочу робити, повинно бути в реальному часі. Хоча ви маєте рацію, що я мав використовувати профілі для виявлення причини ...
Габ Роєр

Річ у тому, що я насправді не звик використовувати профайлер (ми трохи використовували gprof в одному з наших класів, але насправді не заглиблювались ...). Якби ви могли вказати мені деякі джерела, я був би дуже вдячний! PS. Я використовую VS2008
Gab Royer

@Gab: Який у вас VS2008 (Express, Pro ...)? Деякі постачаються з профілем.
sbi

@Gab Вибачте, я більше не використовую VS, тому не можу насправді порадити

@Sbi, з того, що я бачу, це лише у системній версії команди (до якої я маю доступ). Я розгляну це.
Gab Royer

5

Чому ні std::queue? Все, що у нього є, це push_backі pop_front.


3

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

Те саме стосується і списку . Це просто вибір того, який API ви хочете.


Але мені було цікаво, чи постійні push_back роблять чергу або перерозподіл самостійно
Gab Royer

std :: queue - це обгортка навколо іншого контейнера, тому черга, яка обертає деку, буде менш ефективною, ніж необроблена дека.
Джон Міллікін,

1
Для 10 елементів продуктивність, швидше за все, буде такою крихітною проблемою, що "ефективність" може бути краще виміряна в програмістському часі, ніж у кодовому. І дзвінки з черги до deque будь-якою пристойною оптимізацією компілятора були б нульові.
lavinio

2
@ Джон: Я хотів би, щоб ви показали мені набір тестів, що демонструють таку різницю в продуктивності. Він не менш ефективний, ніж сирий деке. Компілятори C ++ вбудовані дуже агресивно.
jalf

3
Я спробував це. : DA quick & dirty 10 elements container with 100000000 pop_front () & push_back () rand () int numbers on Release build for speed on VC9 дає: list (27), queue (6), deque (6), array (8) .
KTC

0

Використовуйте a std::queue, але пам’ятайте про компроміси щодо ефективності двох стандартних Containerкласів.

За замовчуванням std::queueє адаптером поверх std::deque. Як правило, це дасть хорошу продуктивність, якщо у вас невелика кількість черг, що містять велику кількість записів, що, мабуть, є загальним випадком.

Однак не будьте сліпими до реалізації std :: deque . Зокрема:

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

Щоб це визначити, припускаючи, що запис черги - це те, що ви хочете поставити в чергу, тобто досить невеликий за розміром, тоді, якщо у вас є 4 черги, кожна з яких містить 30000 записів, std::dequeреалізація буде варіантом вибору. І навпаки, якщо у вас 30000 черг, кожна з яких містить 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

О, я з якихось причин думав, що може. Не зважай!
Ry-
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.