Uno dei casi più utili che trovo per gli elenchi collegati che lavorano in campi critici per le prestazioni come l'elaborazione di mesh e immagini, motori fisici e raytracing è quando l'utilizzo di elenchi collegati migliora effettivamente la località di riferimento e riduce le allocazioni di heap e talvolta riduce persino l'uso della memoria rispetto a le alternative semplici.
Ora può sembrare un ossimoro completo che le liste collegate potrebbero fare tutto ciò poiché sono famose per fare spesso il contrario, ma hanno una proprietà unica in quanto ogni nodo della lista ha una dimensione fissa e requisiti di allineamento che possiamo sfruttare per consentire per essere immagazzinati in modo contiguo e rimossi in tempo costante in modi che le cose di dimensioni variabili non possono.
Di conseguenza, prendiamo un caso in cui vogliamo fare l'equivalente analogico di memorizzare una sequenza di lunghezza variabile che contiene un milione di sotto-sequenze di lunghezza variabile annidate. Un esempio concreto è una mesh indicizzata che memorizza un milione di poligoni (alcuni triangoli, alcuni quad, alcuni pentagoni, alcuni esagoni, ecc.) Ea volte i poligoni vengono rimossi da qualsiasi punto della mesh e talvolta i poligoni vengono ricostruiti per inserire un vertice in un poligono esistente o rimuoverne uno. In tal caso, se memorizziamo un milione di minuscoli std::vectors
, finiamo per affrontare un'allocazione di heap per ogni singolo vettore e un utilizzo della memoria potenzialmente esplosivo. Un milione di minuscoli SmallVectors
potrebbe non soffrire di questo problema tanto nei casi comuni, ma il loro buffer preallocato che non è allocato separatamente nell'heap potrebbe comunque causare un uso esplosivo della memoria.
Il problema qui è che un milione di std::vector
istanze tenterebbe di memorizzare un milione di cose di lunghezza variabile. Le cose a lunghezza variabile tendono a volere un'allocazione di heap poiché non possono essere archiviate in modo molto efficace in modo contiguo e rimosse a tempo costante (almeno in modo diretto senza un allocatore molto complesso) se non memorizzano il loro contenuto altrove nell'heap.
Se invece facciamo questo:
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;
};
... poi abbiamo ridotto drasticamente il numero di allocazioni di heap e di cache miss. Invece di richiedere un'allocazione di heap e potenziali errori di cache obbligatori per ogni singolo poligono a cui accediamo, ora richiediamo l'allocazione di heap solo quando uno dei due vettori memorizzati nell'intera mesh supera la loro capacità (un costo ammortizzato). E mentre il passo per passare da un vertice al successivo potrebbe ancora causare la sua quota di cache mancate, è ancora spesso minore che se ogni singolo poligono memorizzasse un array dinamico separato poiché i nodi sono memorizzati in modo contiguo e c'è una probabilità che un vertice vicino potrebbe essere accessibile prima dello sfratto (soprattutto considerando che molti poligoni aggiungeranno i loro vertici tutti in una volta, il che rende la maggior parte dei vertici del poligono perfettamente contigui).
Ecco un altro esempio:
... dove le celle della griglia vengono utilizzate per accelerare la collisione particella-particella per, diciamo, 16 milioni di particelle che si muovono ogni singolo fotogramma. In quell'esempio di griglia di particelle, utilizzando elenchi collegati possiamo spostare una particella da una cella della griglia a un'altra semplicemente cambiando 3 indici. La cancellazione da un vettore e il rinvio a un altro può essere notevolmente più costosa e introdurre più allocazioni di heap. Le liste collegate riducono anche la memoria di una cella fino a 32 bit. Un vettore, a seconda dell'implementazione, può preallocare il proprio array dinamico al punto in cui può richiedere 32 byte per un vettore vuoto. Se abbiamo circa un milione di celle della griglia, è una bella differenza.
... ed è qui che trovo gli elenchi collegati più utili in questi giorni, e in particolare trovo utile la varietà "elenco collegato indicizzato" poiché gli indici a 32 bit dimezzano i requisiti di memoria dei collegamenti su macchine a 64 bit e implicano che il i nodi vengono memorizzati in modo contiguo in un array.
Spesso li combino anche con elenchi gratuiti indicizzati per consentire rimozioni e inserimenti a tempo costante ovunque:
In tal caso, l' next
indice punta al successivo indice libero se il nodo è stato rimosso o al successivo indice utilizzato se il nodo non è stato rimosso.
E questo è il caso d'uso numero uno che trovo per gli elenchi collegati in questi giorni. Quando vogliamo memorizzare, diciamo, un milione di sotto-sequenze di lunghezza variabile che media, diciamo, 4 elementi ciascuna (ma a volte con elementi rimossi e aggiunti a una di queste sotto-sequenze), l'elenco collegato ci consente di memorizzare 4 milioni nodi della lista concatenata contigui invece di 1 milione di contenitori che sono ciascuno individualmente allocato nell'heap: un vettore gigante, cioè non un milione di piccoli.