Один из наиболее полезных случаев, которые я нахожу для связанных списков, работающих в областях, критичных к производительности, таких как обработка сеток и изображений, физические движки и трассировка лучей, - это когда использование связанных списков фактически улучшает локальность ссылок и уменьшает выделение кучи, а иногда даже уменьшает использование памяти по сравнению с простые альтернативы.
Теперь это может показаться полным оксюмороном, что связанные списки могут делать все это, поскольку они печально известны тем, что часто делают противоположное, но у них есть уникальное свойство, заключающееся в том, что каждый узел списка имеет фиксированный размер и требования к выравниванию, которые мы можем использовать, чтобы разрешить они должны храниться непрерывно и удаляться в постоянное время способами, недоступными для объектов переменного размера.
В результате давайте возьмем случай, когда мы хотим сделать аналогичный эквивалент сохранения последовательности переменной длины, которая содержит миллион вложенных подпоследовательностей переменной длины. Конкретным примером является индексированная сетка, в которой хранится миллион многоугольников (несколько треугольников, несколько четырехугольников, несколько пятиугольников, несколько шестиугольников и т. Д.), А иногда многоугольники удаляются из любого места сетки, а иногда многоугольники перестраиваются, чтобы вставить вершину в существующий многоугольник или удалить один. В этом случае, если мы сохраним миллион крошечных файлов 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 миллиона контейнеров, каждый из которых выделяется в куче отдельно: один гигантский вектор, то есть не миллион маленьких.