Який сенс використовувати списки над векторами в C ++?


32

Я провів 3 різні експерименти, включаючи списки C ++ та вектори.

Ті, хто має вектори, виявилися ефективнішими, навіть коли було задіяно багато вставок посередині.

Звідси виникає питання: в якому випадку списки мають більше сенсу, ніж вектори?

Якщо вектори здаються більш ефективними в більшості випадків, і, враховуючи, наскільки схожі їх члени, то які переваги залишаються для списків?

  1. Створіть N цілих чисел і покладіть їх у контейнер, щоб контейнер залишився відсортованим. Вставлення було виконано наївно, зчитування елементів по одному та вставлення нового прямо перед першим більшим.
    З переліком час проходить дах, коли розмірність збільшується, порівняно з векторами.

  2. Вставте N цілих чисел у кінці контейнера.
    Для списків та векторів час збільшився на той самий порядок, хоча з векторами він був у 3 рази швидшим.

  3. Вставте N цілих чисел у контейнер.
    Запуск таймера.
    Сортуйте контейнер, використовуючи list.sort для списків, і std :: сортування для векторів. Стоп-таймер.
    Знову ж таки час збільшується на той самий порядок, але з векторами він в середньому в 5 разів швидше.

Я можу продовжувати виконувати тести і наводити пару прикладів, коли списки виявляться кращими.

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

Можливо, ви стикалися з ситуаціями, коли списки було зручніше у використанні чи ефективніше?



1
Ось ще один хороший ресурс на цю тему: stackoverflow.com/a/2209564/8360 також, більшість вказівок на C ++, які я чув, - використовувати вектор за замовчуванням, перелічити лише за наявності конкретної причини.
Захарій Йейтс

Дякую. Однак я не згоден з більшістю того, що сказано в улюбленій відповіді. Більшість цих заздалегідь задуманих ідей було визнано недійсними моїми експериментами. Ця людина не зробила жодного тесту і не застосовувала широко поширену теорію, яку викладали в книгах чи в школі.
Марек Стенлі

1
list, Ймовірно , робить краще , якщо ви видаляєте багато елементів. Я не вірю vector, що коли-небудь поверне пам'ять до системи, поки весь вектор не буде видалений. Також пам’ятайте, що ваш тест №1 не перевіряє час вставки окремо. Це тест, що поєднує пошук і вставку. Це пошук місця, щоб вставити там, де listповільно. Фактична вставка буде швидшою, ніж векторна.
Gort the Robot

3
Настільки типово, що це питання описується з точки зору (часу виконання), продуктивності та лише продуктивності. Це здається сліпою плямою цілого ряду програмістів - вони зосереджуються на цьому аспекті і забувають, що є десятки інших аспектів, які часто бувають набагато важливішими.
Док Браун

Відповіді:


34

Коротка відповідь полягає в тому, що випадків, здається, мало і далеко між ними. Але, мабуть, небагато.

Було б таке, коли вам потрібно зберігати невелику кількість великих об’єктів - особливо, об’єктів, які є настільки великими, що виділяти простір навіть для кількох зайвих з них недоцільно. В основному немає можливості зупинити вектор або деке виділити простір для зайвих об'єктів - це те, як вони визначені (тобто вони повинні виділити додатковий простір для задоволення своїх вимог щодо складності). Якщо ви не можете виділити додатковий простір, std::listможливо, це єдиний стандартний контейнер, який відповідає вашим потребам.

Іншим було б, коли / якщо ви зберігатимете ітератор до "цікавої" точки списку протягом тривалого періоду часу, і коли ви робите вставки та / або видалення, ви (майже) завжди робите це з місця, до якого у вас вже є ітератор, тож ви не пройдетесь по списку, щоб дійти до місця, де ви збираєтеся робити вставку чи видалення. Очевидно те саме стосується, якщо ви працюєте з більш ніж одним місцем, але все ж плануєте зберігати ітератор у кожному місці, з яким ви, швидше за все, працюєте, тому ви маніпулюєте місцями, до яких можна потрапити безпосередньо, і лише рідко пройдете по списку, щоб отримати до тих плям.

Для прикладу першого розглянемо веб-браузер. Він може зберігати пов'язаний список Tabоб'єктів, причому кожен об'єкт вкладки буде представлений на відкритій вкладці в браузері. Кожна вкладка може містити кілька десятків мегабайт даних (ще більше, особливо якщо в ній є щось на зразок відео). Ваша типова кількість відкритих вкладок може бути менше десятка, і 100, ймовірно, близько до верхньої крайності.

Для прикладу другого розглянемо текстовий процесор, який зберігає текст як пов'язаний список глав, кожна з яких може містити пов'язаний список (скажімо) абзаців. Коли користувач редагує, він зазвичай збирається знайти певне місце, де він буде редагуватись, а потім виконати неабияку кількість роботи на цьому місці (або всередині цього абзацу). Так, вони будуть переходити з одного абзацу в інший раз і знову, але в більшості випадків це буде абзац поруч, де вони вже працювали.

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

Зауважте, що в типовому випадку це, ймовірно, відповідає і першому критерію - глава містить досить невелику кількість абзаців, кожен з яких, ймовірно, буде досить великим (принаймні щодо розміру покажчиків у вузол тощо. Так само у вас є відносно невелика кількість глав, кожна з яких може бути кілька кілобайт.

З цього приводу я маю визнати, що обидва ці приклади, мабуть, трохи надумані, і хоча пов'язаний список може працювати цілком добре для будь-якого, він, ймовірно, не забезпечить величезної переваги в будь-якому випадку. Наприклад, в обох випадках виділення додаткового місця у векторі для будь-яких (порожніх) веб-сторінок / вкладок або для порожніх розділів навряд чи буде реальною проблемою.


4
+1, але: Перший випадок зникає, коли ви використовуєте вказівники, які ви завжди повинні використовувати з великими об'єктами. Зв'язані списки також не підходять для прикладу другого; масиви власні для всіх операцій, коли вони такі короткі.
amara

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

Існує багато застосувань для пов'язаних списків - це просто те, що вони не такі поширені, як динамічні масиви. Кеш LRU - це одне поширене використання пов'язаного списку.
Чарльз Сальвія

Також std::vector<std::unique_ptr<T>>може бути хорошою альтернативою.
Дедуплікатор

24

За словами самого Bjarne Stroustrup, вектори завжди повинні бути колекцією за замовчуванням для послідовностей даних. Ви можете вибрати список, якщо хочете оптимізувати для вставки та видалення елементів, але зазвичай цього не слід. Витрати на перелік - це повільне проходження та використання пам'яті.

Про це він розповідає в цій презентації .

Близько 0:44 він розповідає про вектори та списки загалом.

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

Близько 1:08 йому дають запитання щодо цього питання.

Що ми повинні бачити, що нам потрібна послідовність елементів. А послідовність елементів за замовчуванням елементів у C ++ - вектор. Тепер, тому що це компактно і ефективно. Реалізація, відображення обладнання до обладнання. Тепер, якщо ви хочете оптимізувати для вставки та видалення - ви говорите: "ну, я не хочу версії послідовності за замовчуванням. Я хочу спеціалізований, який є списком '. І якщо ви це зробите, ви повинні досить знати, щоб сказати: "Я приймаю деякі витрати та деякі проблеми, як-от повільні обходи та більше використання пам'яті".


1
Ви б не хотіли коротко написати те, що сказано у презентації, на яку ви посилаєтесь "приблизно в 0:44 та 1:08"?
гнат

2
@gnat - звичайно. Я намагався цитувати речі, які мають сенс окремо, і для цього потрібен контекст слайдів.
Піт

11

Єдине місце, де я зазвичай використовую списки - це те, де мені потрібно стерти елементи, а не визнати недійсними ітератори. std::vectorвизнає недійсним всі ітератори при вставці та стиранні. std::listгарантує, що ітератори для існуючих елементів залишаються дійсними після вставки або видалення.


4

На додаток до інших наданих відповідей, списки мають певні особливості, які не існують у векторах (оскільки це було б шалено дорого.) Операції злиття та злиття є найбільш значущими. Якщо у вас часто є маса списків, які потрібно додати або об'єднати разом, список, мабуть, є хорошим вибором.

Але якщо вам не потрібно виконувати ці операції, то, ймовірно, ні.


3

Відсутність притаманного кешу / зручності для сторінок зв'язаних списків, як правило, робить їх майже повністю відхиленими багатьма розробниками C ++, і з хорошим виправданням у цій формі за замовчуванням.

Пов'язані списки все ще можуть бути чудовими

І все ж пов'язані списки можуть бути чудовими, коли їх підтримує фіксований розподільник, який повертає їм ту просторову локальність, яка їм властива.

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

Простий прискорювач сітки

В якості практичного прикладу розглянемо 2D візуальне моделювання. У ньому є екран прокрутки з картою, яка охоплює 400x400 (160 000 комірок сітки), які використовуються для прискорення таких речей, як виявлення зіткнень між мільйонами частинок, що рухаються навколо кожного кадру (ми уникаємо чотирьох дерев тут, оскільки вони насправді мають гірший результат при цьому рівні динамічні дані). Ціла купа частинок постійно рухається навколо кожного кадру, тобто постійно переходить від однієї комірки сітки до іншої.

У цьому випадку, якщо кожна частинка є вузлом списку, що пов'язаний між собою, кожна комірка сітки може починатись як лише headвказівник, на який вказує nullptr. Коли народиться нова частинка, ми просто помістимо її в клітинку сітки, в якій вона знаходиться, встановивши headвказівник цієї комірки, щоб вказати на цей вузол частинки. Коли частинка рухається від однієї комірки сітки до іншої, ми просто маніпулюємо покажчиками.

Це може бути набагато ефективніше, ніж зберігати 160 000 vectorsдля кожної комірки сітки, а відштовхуватись та стиратися з середини весь час за кадром.

std :: список

Це стосується ручного прокручування, нав'язливих, поодиноко пов'язаних списків, підкріплених фіксованим розподільником. std::listявляє собою подвійно пов'язаний список, і він може бути не настільки компактним, коли порожній, як єдиний вказівник (залежить від реалізації постачальника), плюс це біль для впровадження користувальницьких розподільників уstd::allocator формі.

Я мушу визнати, що ніколи listнічим не користуюся . Але пов'язані списки можуть бути все-таки чудовими! Однак вони не прекрасні з тих причин, що люди часто спокушаються ними користуватися, і не такі чудові, якщо їх не підтримує дуже ефективний фіксований розподільник, який пом'якшує безліч принаймні обов'язкових помилок сторінки та пов'язаних з ними помилок кеша.


1
З C ++ 11, існує стандартний список, пов’язаний окремо std::forward_list.
sharyex

2

Вам слід врахувати розмір елементів у контейнері.

int вектор елементів дуже швидкий, оскільки більша частина даних вміщується в кеш процесора (і SIMD-інструкції, ймовірно, можуть використовуватися для копіювання даних).

Якщо розмір елемента більший, то результат випробувань 1 і 3 може суттєво змінитися.

З дуже всебічного порівняння продуктивності :

З цього можна зробити прості висновки щодо використання кожної структури даних:

  • Кількість хрускіт: використання std::vectorабоstd::deque
  • Лінійний пошук: використовувати std::vectorабоstd::deque
  • Випадкове вставлення / видалення:
    • Невеликий розмір даних: використання std::vector
    • Великий розмір елементів: використання std::list(за винятком випадків, коли призначено головним чином для пошуку)
  • Нетривіальний тип даних: використовуйте, std::listякщо контейнер не потрібен спеціально для пошуку. Але для декількох модифікацій контейнера це буде дуже повільно.
  • Натисніть на фронт: використовуйте std::dequeабоstd::list

(як бічна примітка std::deque- дуже занижена структура даних).

З точки зору зручності std::listдає гарантію, що ітератори ніколи не будуть визнані недійсними під час вставлення та видалення інших елементів. Це часто є ключовим аспектом.


2

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

Це не стосується списків.

Точні правила для всіх стандартних контейнерів наведені в цій публікації StackOverflow .


0

Коротше кажучи, немає вагомих причин для використання std::list<>:

  • Якщо вам потрібен несортований контейнер, std::vector<> правила.
    (Видаліть елементи, замінивши їх останнім елементом вектора.)

  • Якщо вам потрібен відсортований контейнер, std::vector<shared_ptr<>> правила.

  • Якщо вам потрібен розріджений індекс, std::unordered_map<> правила.

Це воно.

Я вважаю, що існує лише одна ситуація, коли я схильний використовувати пов'язаний список: Коли у мене є попередньо існуючі об'єкти, які потрібно певним чином з'єднати, щоб реалізувати якусь додаткову логіку програми. Однак у такому випадку я ніколи не використовую std::list<>, скоріше вдаюсь до (розумного) наступного вказівника всередині об’єкта, тим більше, що більшість випадків використання призводить до дерева, а не до лінійного списку. В одних випадках отримана структура є пов'язаним списком, в інших - це дерево або спрямований ациклічний графік. Основна мета цих покажчиків - завжди будувати логічну структуру, ніколи не керувати об'єктами. У нас є std::vector<>для цього.


-1

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

Значне використання списків - це коли вам потрібно підтримувати видалення елементів під час ітерації. Коли вектор модифікований, всі ітератори є (потенційно) недійсними. Зі списку недійсним є лише ітератор для видаленого елемента. Усі інші ітератори залишаються дійсними.

Типовий порядок використання контейнерів - вектор, деке, потім список. Вибір контейнера зазвичай ґрунтується на push_back select vector, pop_front select deque, insert select list.


3
при вилученні елементів під час ітерації зазвичай краще використовувати вектор і просто створити новий вектор для результатів
amara

-1

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

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

Я не знаю, чи це головне питання, але це було причиною, яку мені дали в роботі (розвиток мобільних ігор), щоб уникнути векторів.


1
ні, вектор буде копіювати, і це дорого. Але подорож пов'язаним списком (щоб зрозуміти, куди потрібно вставити) - теж дорого. Ключ дійсно для вимірювання
Кейт Григорій

@KateGregory Я мав на увазі додатково до цього, Дозвольте редагувати відповідно
Karthik T

3
Правильно, але вірите чи ні (і більшість людей не вірить у нього) вартість, яку ви не згадали, проїжджаючи пов'язаний список, щоб знайти, куди вставити ВНУТРІШНІ копії (особливо якщо елементи невеликі (або рухомі, тому що тоді вони рухаються)), а вектор часто (або навіть зазвичай) швидший. Хочеш - вір, хочеш - ні.
Кейт Григорій
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.