Uno de los casos más útiles que encuentro para las listas vinculadas que trabajan en campos críticos para el rendimiento como el procesamiento de imágenes y mallas, motores de física y trazado de rayos es cuando el uso de listas vinculadas en realidad mejora la localidad de referencia y reduce las asignaciones de montón y, a veces, incluso reduce el uso de memoria en comparación con las alternativas sencillas.
Ahora, eso puede parecer un completo oxímoron de que las listas enlazadas podrían hacer todo eso, ya que son conocidas por hacer a menudo lo contrario, pero tienen una propiedad única en el sentido de que cada nodo de lista tiene un tamaño fijo y requisitos de alineación que podemos aprovechar para permitir. para que se almacenen de forma contigua y se eliminen en tiempo constante de formas que las cosas de tamaño variable no pueden.
Como resultado, tomemos un caso en el que queremos hacer el equivalente analógico de almacenar una secuencia de longitud variable que contiene un millón de subsecuencias de longitud variable anidadas. Un ejemplo concreto es una malla indexada que almacena un millón de polígonos (algunos triángulos, algunos quads, algunos pentágonos, algunos hexágonos, etc.) y, a veces, los polígonos se eliminan de cualquier lugar de la malla y, a veces, los polígonos se reconstruyen para insertar un vértice en un polígono existente o quitar uno. En ese caso, si almacenamos un millón de pequeños std::vectors
, terminamos enfrentando una asignación de montón para cada vector, así como un uso de memoria potencialmente explosivo. Un millón de minúsculos SmallVectors
podría no sufrir este problema tanto en los casos comunes, pero entonces su búfer preasignado que no está asignado al montón por separado podría causar un uso explosivo de la memoria.
El problema aquí es que un millón de std::vector
instancias intentarían almacenar un millón de cosas de longitud variable. Las cosas de longitud variable tienden a querer una asignación de montón, ya que no se pueden almacenar de manera muy efectiva de forma contigua y eliminar en tiempo constante (al menos de una manera sencilla sin un asignador muy complejo) si no almacenaron su contenido en otra parte del montón.
Si, en cambio, hacemos esto:
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;
};
... luego hemos reducido drásticamente el número de asignaciones de montón y fallas de caché. En lugar de requerir una asignación de almacenamiento dinámico y pérdidas de caché potencialmente obligatorias para cada polígono al que accedemos, ahora solo requerimos esa asignación de almacenamiento dinámico cuando uno de los dos vectores almacenados en toda la malla excede su capacidad (un costo amortizado). Y aunque la zancada para ir de un vértice al siguiente todavía puede causar su parte de fallas de caché, a menudo es menor que si cada polígono almacenara una matriz dinámica separada, ya que los nodos se almacenan contiguamente y existe la probabilidad de que un vértice vecino pueda ser accedido antes del desalojo (especialmente considerando que muchos polígonos agregarán sus vértices todos a la vez, lo que hace que la mayor parte de los vértices de los polígonos sean perfectamente contiguos).
Aquí hay otro ejemplo:
... donde las celdas de la cuadrícula se utilizan para acelerar la colisión entre partículas para, digamos, 16 millones de partículas que se mueven en cada cuadro. En ese ejemplo de cuadrícula de partículas, usando listas vinculadas podemos mover una partícula de una celda de cuadrícula a otra simplemente cambiando 3 índices. Borrar de un vector y regresar a otro puede ser considerablemente más caro e introducir más asignaciones de montón. Las listas enlazadas también reducen la memoria de una celda a 32 bits. Un vector, dependiendo de la implementación, puede preasignar su matriz dinámica hasta el punto en que puede tomar 32 bytes para un vector vacío. Si tenemos alrededor de un millón de celdas de cuadrícula, eso es una gran diferencia.
... y aquí es donde encuentro que las listas vinculadas son más útiles en estos días, y específicamente encuentro útil la variedad de "listas vinculadas indexadas" ya que los índices de 32 bits reducen a la mitad los requisitos de memoria de los vínculos en máquinas de 64 bits e implican que el los nodos se almacenan de forma contigua en una matriz.
A menudo, también los combino con listas gratuitas indexadas para permitir eliminaciones e inserciones en tiempo constante en cualquier lugar:
En ese caso, el next
índice apunta al siguiente índice libre si el nodo se ha eliminado o al siguiente índice utilizado si el nodo no se ha eliminado.
Y este es el caso de uso número uno que encuentro para las listas enlazadas en estos días. Cuando queremos almacenar, digamos, un millón de subsecuencias de longitud variable promediando, digamos, 4 elementos cada una (pero a veces con elementos que se eliminan y se agregan a una de estas subsecuencias), la lista enlazada nos permite almacenar 4 millones nodos de lista enlazados de forma contigua en lugar de 1 millón de contenedores, cada uno de los cuales se asigna individualmente al montón: un vector gigante, es decir, no un millón de pequeños.