Один з найкорисніших випадків, які я вважаю для пов'язаних списків, які працюють у критичних для продуктивності сферах, таких як обробка сітки та зображень, фізичні двигуни та проміння, - це коли використання зв'язаних списків фактично покращує місцеположення та зменшує розподіл купи, а іноді навіть зменшує використання пам'яті порівняно з прямі альтернативи.
Тепер це може здатися повним оксимороном, що зв'язані списки можуть зробити все це, оскільки вони відомі тим, що часто роблять навпаки, але вони мають унікальну властивість у тому, що кожен вузол списку має фіксований розмір та вимоги до вирівнювання, які ми можемо використовувати, щоб дозволити їх потрібно постійно зберігати та вилучати постійно, таким чином, щоб речі змінного розміру не могли.
В результаті візьмемо випадок, коли ми хочемо зробити аналогічний еквівалент зберігання послідовності змінної довжини, яка містить мільйон вкладених підрядів змінної довжини. Конкретний приклад - індексована сітка, яка зберігає мільйон багатокутників (деякі трикутники, кілька квадратиків, деякі п’ятикутники, деякі шестикутники тощо), а іноді багатокутники видаляються з будь-якого місця в сітці, а іноді багатокутники відновлюються, щоб вставити вершину до існуючого багатокутника або видалити одну. У такому випадку, якщо ми зберігаємо мільйон крихітних std::vectors
, ми стикаємося з куповим виділенням для кожного окремого вектора, а також з потенційно вибухонебезпечним використанням пам'яті. Мільйон крихітних SmallVectors
може не постраждати від цієї проблеми так само часто, як правило, але тоді їх попередньо виділений буфер, який не виділений окремо з купи, все ще може спричинити використання вибухової пам'яті.
Проблема тут полягає в тому, що мільйон std::vector
примірників намагається зберігати мільйон речей різної довжини. Речі змінної довжини, як правило, хочуть виділити купу, оскільки вони не можуть дуже ефективно зберігатись та видалятися постійно (принаймні прямолінійно, без дуже складного розподільника), якщо вони не зберігали їх вміст в іншому купі.
Якщо натомість ми робимо це:
struct FaceVertex
{
// Points to next vertex in polygon or -1
// if we're at the end of the polygon.
int next;
...
};
struct Polygon
{
// Points to first vertex in polygon.
int first_vertex;
...
};
struct Mesh
{
// Stores all the face vertices for all polygons.
std::vector<FaceVertex> fvs;
// Stores all the polygons.
std::vector<Polygon> polys;
};
... тоді ми різко скоротили кількість виділень купи та пропуски кешу. Замість того, щоб вимагати розподілу купи та потенційно обов'язкових пропусків кешу для кожного полігону, до якого ми отримуємо доступ, ми вимагаємо лише виділення купи, коли один з двох векторів, що зберігаються у всій сітці, перевищує їх потужність (амортизована вартість). І хоча кроки переходу від однієї вершини до іншої все ще можуть спричинити промаху її частки кешу, це все ще часто менше, ніж якби кожен полігон зберігав окремий динамічний масив, оскільки вузли зберігаються безперервно і є ймовірність, що сусідня вершина може отримати доступ до виселення (особливо враховуючи, що багато полігонів додадуть свої вершини відразу, що робить левову частку багатокутних вершин ідеально суміжними).
Ось ще один приклад:
... де осередки сітки використовуються для прискорення зіткнення частинок-частинок для, скажімо, 16 мільйонів частинок, що рухаються кожен кадр. У цьому прикладі сітки частинок, використовуючи пов'язані списки, ми можемо перемістити частинку з однієї комірки сітки в іншу, просто змінивши 3 індексу. Стирання з вектора і відведення назад до іншого може бути значно дорожчим і ввести більше купових виділень. Зв'язані списки також зменшують пам'ять комірки до 32 біт. Вектор, залежно від реалізації, може попередньо розподілити свій динамічний масив до тієї точки, де він може взяти 32 байти для порожнього вектора. Якщо у нас є близько мільйона комірок сітки, це зовсім різниця.
... і саме тут я знаходжу пов'язані списки найкориснішими в наші дні, і я конкретно вважаю різноманітність "індексованого пов'язаного списку" корисною, оскільки 32-бітні індекси наполовину знижують вимоги до пам'яті посилань на 64-бітних машинах, і вони означають, що вузли постійно зберігаються в масиві.
Часто я також комбіную їх з індексованими безкоштовними списками, щоб дозволити видалення та вставки постійного часу в будь-якому місці:
У цьому випадку next
індекс або вказує на наступний вільний індекс, якщо вузол був видалений, або наступний використаний індекс, якщо вузол не був видалений.
І це є випадком використання номер один, який я знаходжу для пов’язаних списків сьогодні. Коли ми хочемо зберегти, скажімо, мільйон підрядних послідовностей змінної довжини, що в середньому становлять, скажімо, 4 елементи кожен (але іноді з елементами видаляються та додаються до однієї з цих підрядів), пов'язаний список дозволяє нам зберігати 4 мільйони зв'язані вузли списку безперервно замість 1 мільйона контейнерів, кожен з яких виділений окремо в купу: один гігантський вектор, тобто не мільйон малих.