Un po 'di eco al suggerimento di Kylotan ma consiglierei di risolverlo a livello di struttura dati quando possibile, non a livello di allocatore inferiore se puoi aiutarlo.
Ecco un semplice esempio di come puoi evitare di allocare e liberare Foos
ripetutamente usando un array con buchi con elementi collegati insieme (risolvendolo a livello di "contenitore" anziché a livello di "allocatore"):
struct FooNode
{
explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}
// Stores a 'Foo'.
Foo element;
// Points to the next foo available; either the
// next used foo or the next deleted foo. Can
// use SoA and hoist this out if Foo doesn't
// have 32-bit alignment.
int next;
};
struct Foos
{
// Stores all the Foo nodes.
vector<FooNode> nodes;
// Points to the first used node.
int first_node;
// Points to the first free node.
int free_node;
Foos(): first_node(-1), free_node(-1)
{
}
const FooNode& operator[](int n) const
{
return data[n];
}
void insert(const Foo& element)
{
int index = free_node;
if (index != -1)
{
// If there's a free node available,
// pop it from the free list, overwrite it,
// and push it to the used list.
free_node = data[index].next;
data[index].next = first_node;
data[index].element = element;
first_node = index;
}
else
{
// If there's no free node available, add a
// new node and push it to the used list.
FooNode new_node(element);
new_node.next = first_node;
first_node = data.size() - 1;
data.push_back(new_node);
}
}
void erase(int n)
{
// If the node being removed is the first used
// node, pop it from the used list.
if (first_node == n)
first_node = data[n].next;
// Push the node to the free list.
data[n].next = free_node;
free_node = n;
}
};
Qualcosa a tal fine: un elenco di indici collegati singolarmente con un elenco gratuito. I collegamenti di indice consentono di saltare gli elementi rimossi, rimuovere gli elementi in tempo costante e anche recuperare / riutilizzare / sovrascrivere gli elementi liberi con l'inserimento in tempo costante. Per scorrere la struttura, fai qualcosa del genere:
for (int index = foos.first_node; index != -1; index = foos[index].next)
// do something with foos[index]
E puoi generalizzare il suddetto tipo di struttura di dati "array collegato di fori" utilizzando modelli, posizionando invocazioni di nuovi e manuali manuali per evitare il requisito di assegnazione di copie, farlo invocare distruttori quando gli elementi vengono rimossi, fornire un iteratore in avanti, ecc. I ho scelto di mantenere l'esempio molto simile a C per illustrare più chiaramente il concetto e anche perché sono molto pigro.
Detto questo, questa struttura tende a degradare nella località spaziale dopo aver rimosso e inserito molte cose da / verso il centro. A quel punto i next
collegamenti potrebbero farti camminare avanti e indietro lungo il vettore, ricaricando i dati precedentemente sfrattati da una linea di cache all'interno dello stesso attraversamento sequenziale (questo è inevitabile con qualsiasi struttura di dati o allocatore che consente la rimozione a tempo costante senza mescolare gli elementi durante il recupero spazi dal centro con inserimento a tempo costante e senza usare qualcosa come un bitset parallelo o una removed
bandiera). Per ripristinare la compatibilità con la cache, è possibile implementare un metodo ctor e swap di copia come questo:
Foos(const Foos& other)
{
for (int index = other.first_node; index != -1; index = other[index].next)
insert(foos[index].element);
}
void Foos::swap(Foos& other)
{
nodes.swap(other.nodes):
std::swap(first_node, other.first_node);
std::swap(free_node, other.free_node);
}
// ... then just copy and swap:
Foos(foos).swap(foos);
Ora la nuova versione è di nuovo compatibile con la cache da attraversare. Un altro metodo è memorizzare un elenco separato di indici nella struttura e ordinarli periodicamente. Un altro è usare un bitset per indicare quali indici sono usati. Ciò ti farà sempre attraversare il bitset in ordine sequenziale (per farlo in modo efficiente, controlla 64 bit alla volta, ad es. Usando FFS / FFZ). Il bitset è il più efficiente e non invadente, richiede solo un bit parallelo per elemento per indicare quali vengono utilizzati e quali vengono rimossi invece di richiedere next
indici a 32 bit , ma il più dispendioso in termini di tempo per scrivere bene (non lo farà sii veloce per l'attraversamento se stai controllando un bit alla volta - hai bisogno di FFS / FFZ per trovare un bit impostato o non impostato immediatamente tra più di 32 bit alla volta per determinare rapidamente intervalli di indici occupati).
Questa soluzione collegata è generalmente la più semplice da implementare e non intrusiva (non richiede la modifica Foo
per memorizzare alcuni removed
flag), il che è utile se si desidera generalizzare questo contenitore per lavorare con qualsiasi tipo di dati se non ti dispiace che a 32 bit spese generali per elemento.
Devo creare un pool di memoria per l'allocazione dinamica o non è necessario preoccuparsi di questo? Cosa succede se la piattaforma di destinazione sono dispositivi mobili?
bisogno è una parola forte e sto lavorando di parte in aree molto critiche per le prestazioni come raytracing, elaborazione di immagini, simulazioni di particelle ed elaborazione di mesh, ma è relativamente molto costoso allocare e liberare oggetti teen usati per l'elaborazione molto leggera come i proiettili e particelle individualmente contro un allocatore di memoria di dimensioni variabili per scopi generici. Dato che dovresti essere in grado di generalizzare la struttura di dati di cui sopra in un giorno o due per archiviare tutto ciò che desideri, penso che sarebbe uno scambio utile eliminare completamente tali costi di allocazione / deallocazione dell'heap da pagare per ogni singola cosa da teenager. Oltre a ridurre i costi di allocazione / deallocazione, si ottiene una migliore località di riferimento che attraversa i risultati (meno errori nella cache e errori di pagina, ad esempio).
Per quanto riguarda ciò che Josh ha menzionato di GC, non ho studiato l'implementazione GC di C # con la stessa precisione di quella di Java, ma gli allocatori GC hanno spesso un allocazione inizialeè molto veloce perché utilizza un allocatore sequenziale che non può liberare memoria dal centro (quasi come uno stack, non è possibile eliminare le cose dal centro). Quindi paga per i costi costosi consentire effettivamente la rimozione di singoli oggetti in un thread separato copiando la memoria e eliminando la memoria precedentemente allocata nel suo insieme (come distruggere l'intero stack contemporaneamente mentre copiando i dati in qualcosa di più simile a una struttura collegata), ma poiché è fatto in un thread separato, non necessariamente blocca così tanto i thread dell'applicazione. Tuttavia, ciò comporta un costo nascosto molto significativo di un ulteriore livello di indiretta e della perdita generale di LOR dopo un ciclo GC iniziale. Tuttavia, è un'altra strategia per accelerare l'allocazione: renderla più economica nel thread di chiamata e quindi svolgere il lavoro costoso in un'altra. Per questo sono necessari due livelli di riferimento indiretto per fare riferimento ai tuoi oggetti anziché uno poiché finiranno per essere rimescolati nella memoria tra il momento in cui si allocano inizialmente e dopo un primo ciclo.
Un'altra strategia in una prospettiva simile che è un po 'più facile da applicare in C ++ è semplicemente non preoccuparti di liberare i tuoi oggetti nei thread principali. Continuando semplicemente ad aggiungere e aggiungere e aggiungere alla fine di una struttura di dati che non consente di rimuovere le cose dal centro. Tuttavia, contrassegnare quelle cose che devono essere rimosse. Quindi un thread separato potrebbe occuparsi del costoso lavoro di creazione di una nuova struttura di dati senza gli elementi rimossi e quindi scambiare atomicamente quello nuovo con quello vecchio, ad es. Gran parte del costo di allocazione e liberazione degli elementi può essere trasferito a un thread separato se è possibile supporre che la richiesta di rimozione di un elemento non debba essere soddisfatta immediatamente. Ciò non solo rende la liberazione più economica per quanto riguarda i thread, ma rende l'allocazione più economica, poiché puoi utilizzare una struttura di dati molto più semplice e più stupida che non deve mai gestire i casi di rimozione dal centro. È come un contenitore che ha solo bisogno di unpush_back
funzione per inserimento, una clear
funzione per rimuovere tutti gli elementi e swap
scambiare i contenuti con un nuovo contenitore compatto esclusi gli elementi rimossi; per quanto riguarda le mutazioni.