L'un des cas les plus utiles que je trouve pour les listes liées travaillant dans des domaines critiques pour les performances tels que le traitement de maillage et d'image, les moteurs physiques et le lancer de rayons est lorsque l'utilisation de listes liées améliore réellement la localité de référence et réduit les allocations de tas et parfois même réduit l'utilisation de la mémoire par rapport à les alternatives simples.
Maintenant, cela peut sembler être un oxymore complet que les listes liées pourraient faire tout cela car elles sont connues pour faire souvent le contraire, mais elles ont une propriété unique en ce que chaque nœud de liste a une taille et des exigences d'alignement fixes que nous pouvons exploiter pour permettre ils doivent être stockés de manière contiguë et supprimés en temps constant d'une manière que les choses de taille variable ne peuvent pas.
En conséquence, prenons un cas où nous voulons faire l'équivalent analogique de stocker une séquence de longueur variable qui contient un million de sous-séquences de longueur variable imbriquées. Un exemple concret est un maillage indexé stockant un million de polygones (certains triangles, certains quads, certains pentagones, certains hexagones, etc.) et parfois des polygones sont supprimés de n'importe où dans le maillage et parfois des polygones sont reconstruits pour insérer un sommet dans un polygone existant ou supprimer un. Dans ce cas, si nous stockons un million de minuscules std::vectors
, nous nous retrouvons face à une allocation de tas pour chaque vecteur unique ainsi qu'à une utilisation de la mémoire potentiellement explosive. Un million de minuscules SmallVectors
peuvent ne pas souffrir autant de ce problème dans les cas courants, mais leur tampon préalloué qui n'est pas alloué séparément en tas peut toujours provoquer une utilisation explosive de la mémoire.
Le problème ici est qu'un million d' std::vector
instances essaieraient de stocker un million de choses de longueur variable. Les objets de longueur variable ont tendance à vouloir une allocation de tas car ils ne peuvent pas très efficacement être stockés de manière contiguë et supprimés en temps constant (au moins de manière simple sans allocateur très complexe) s'ils ne stockent pas leur contenu ailleurs sur le tas.
Si, à la place, nous faisons ceci:
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;
};
... alors nous avons considérablement réduit le nombre d'allocations de tas et d'erreurs de cache. Au lieu d'exiger une allocation de tas et des échecs de cache potentiellement obligatoires pour chaque polygone auquel nous accédons, nous n'exigeons désormais cette allocation de tas que lorsque l'un des deux vecteurs stockés dans le maillage entier dépasse leur capacité (un coût amorti). Et bien que la foulée pour passer d'un sommet à l'autre puisse toujours causer sa part de ratés dans le cache, c'est encore souvent moins que si chaque polygone stockait un tableau dynamique séparé car les nœuds sont stockés de manière contiguë et il y a une probabilité qu'un sommet voisin puisse accessible avant l'expulsion (d'autant plus que de nombreux polygones ajouteront leurs sommets en même temps, ce qui rend la part du lion des sommets de polygones parfaitement contigus).
Voici un autre exemple:
... où les cellules de la grille sont utilisées pour accélérer la collision particule-particule pour, par exemple, 16 millions de particules se déplaçant à chaque image. Dans cet exemple de grille de particules, en utilisant des listes liées, nous pouvons déplacer une particule d'une cellule de la grille à une autre en changeant simplement 3 indices. L'effacement d'un vecteur et le repoussement vers un autre peuvent être considérablement plus coûteux et introduire plus d'allocations de tas. Les listes chaînées réduisent également la mémoire d'une cellule à 32 bits. Un vecteur, selon l'implémentation, peut préallouer son tableau dynamique au point où il peut prendre 32 octets pour un vecteur vide. Si nous avons environ un million de cellules de grille, c'est toute une différence.
... et c'est là que je trouve les listes chaînées les plus utiles de nos jours, et je trouve spécifiquement la variété «liste chaînée indexée» utile puisque les indices 32 bits divisent par deux les besoins en mémoire des liens sur les machines 64 bits et ils impliquent que le les nœuds sont stockés de manière contiguë dans un tableau.
Souvent, je les combine également avec des listes gratuites indexées pour permettre des suppressions et des insertions à temps constant n'importe où:
Dans ce cas, l' next
index pointe vers le prochain index libre si le nœud a été supprimé ou vers le prochain index utilisé si le nœud n'a pas été supprimé.
Et c'est le cas d'utilisation numéro un que je trouve pour les listes liées de nos jours. Lorsque nous voulons stocker, par exemple, un million de sous-séquences de longueur variable faisant en moyenne, par exemple, 4 éléments chacun (mais parfois avec des éléments supprimés et ajoutés à l'une de ces sous-séquences), la liste chaînée nous permet de stocker 4 millions nœuds de liste chaînée contigus au lieu de 1 million de conteneurs qui sont chacun alloués individuellement par tas: un vecteur géant, c'est-à-dire pas un million de petits.