Um dos casos mais úteis que encontro para listas vinculadas que trabalham em campos de desempenho crítico, como processamento de malha e imagem, mecanismos de física e raytracing, é quando o uso de listas vinculadas realmente melhora a localidade de referência e reduz as alocações de heap e às vezes até reduz o uso de memória em comparação com as alternativas diretas.
Isso pode parecer um oxímoro completo que as listas vinculadas podem fazer tudo isso, já que são conhecidas por fazerem frequentemente o contrário, mas têm uma propriedade única em que cada nó da lista tem um tamanho fixo e requisitos de alinhamento que podemos explorar para permitir eles devem ser armazenados contiguamente e removidos em tempo constante de maneiras que coisas de tamanho variável não podem.
Como resultado, vamos pegar um caso em que queremos fazer o equivalente analógico de armazenar uma sequência de comprimento variável que contém um milhão de sub-sequências de comprimento variável aninhadas. Um exemplo concreto é uma malha indexada que armazena um milhão de polígonos (alguns triângulos, alguns quadrantes, alguns pentágonos, alguns hexágonos, etc.) e às vezes polígonos são removidos de qualquer lugar da malha e, às vezes, polígonos são reconstruídos para inserir um vértice em um polígono existente ou remova um. Nesse caso, se armazenarmos um milhão de minúsculos std::vectors
, acabaremos enfrentando uma alocação de heap para cada vetor único, bem como um uso de memória potencialmente explosivo. Um milhão de tiny SmallVectors
pode não sofrer esse problema tanto em casos comuns, mas seu buffer pré-alocado que não é alocado em heap separadamente pode ainda causar uso de memória explosiva.
O problema aqui é que um milhão de std::vector
instâncias estariam tentando armazenar um milhão de coisas de comprimento variável. Coisas de comprimento variável tendem a querer uma alocação de heap, uma vez que não podem ser armazenados de forma muito eficaz de forma contígua e removidas em tempo constante (pelo menos de forma direta, sem um alocador muito complexo) se não armazenarem seu conteúdo em outro lugar no heap.
Se, em vez disso, fizermos isso:
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;
};
... reduzimos drasticamente o número de alocações de heap e perdas de cache. Em vez de exigir uma alocação de heap e perdas de cache potencialmente obrigatórias para cada polígono que acessamos, agora exigimos apenas essa alocação de heap quando um dos dois vetores armazenados em toda a malha excede sua capacidade (um custo amortizado). E embora o passo para ir de um vértice ao próximo ainda possa causar sua parcela de perdas de cache, ainda é muitas vezes menor do que se cada polígono armazenasse uma matriz dinâmica separada, uma vez que os nós são armazenados de forma contígua e há uma probabilidade de que um vértice vizinho possa ser acessado antes do despejo (especialmente considerando que muitos polígonos adicionarão seus vértices todos de uma vez, o que torna a maior parte dos vértices poligonais perfeitamente contíguos).
Aqui está outro exemplo:
... onde as células da grade são usadas para acelerar a colisão partícula-partícula para, digamos, 16 milhões de partículas movendo-se a cada quadro. Nesse exemplo de grade de partículas, usando listas vinculadas, podemos mover uma partícula de uma célula da grade para outra apenas alterando 3 índices. Apagar de um vetor e enviar para outro pode ser consideravelmente mais caro e introduzir mais alocações de heap. As listas vinculadas também reduzem a memória de uma célula para 32 bits. Um vetor, dependendo da implementação, pode pré-alocar sua matriz dinâmica ao ponto em que pode levar 32 bytes para um vetor vazio. Se temos cerca de um milhão de células de grade, isso é uma grande diferença.
... e é aqui que considero as listas vinculadas mais úteis atualmente, e especificamente acho a variedade "lista vinculada indexada" útil, pois os índices de 32 bits reduzem pela metade os requisitos de memória dos links em máquinas de 64 bits e implicam que o os nós são armazenados de forma contígua em uma matriz.
Freqüentemente, também os combino com listas livres indexadas para permitir remoções e inserções em tempo constante em qualquer lugar:
Nesse caso, o next
índice aponta para o próximo índice livre se o nó foi removido ou o próximo índice usado se o nó não foi removido.
E este é o caso de uso número um que encontro para listas vinculadas atualmente. Quando queremos armazenar, digamos, um milhão de subseqüências de comprimento variável com uma média de, digamos, 4 elementos cada (mas às vezes com elementos sendo removidos e adicionados a uma dessas subseqüências), a lista vinculada nos permite armazenar 4 milhões nós da lista encadeada contiguamente em vez de 1 milhão de contêineres, cada um individualmente alocado em heap: um vetor gigante, ou seja, não um milhão de pequenos.