Es ist eine Art Echo auf Kylotans Vorschlag, aber ich würde empfehlen, dies auf der Datenstrukturebene zu lösen, wenn möglich, nicht auf der unteren Allokatorebene, wenn Sie helfen können.
Hier ein einfaches Beispiel, wie Sie vermeiden können, Foos
ein Array mit Löchern und miteinander verknüpften Elementen wiederholt zuzuweisen und freizugeben (dies auf "Container" -Ebene anstelle einer "Allokator" -Ebene zu lösen):
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;
}
};
Etwas in diesem Sinne: eine einfach verknüpfte Indexliste mit einer freien Liste. Mit den Index-Links können Sie entfernte Elemente überspringen, Elemente in konstanter Zeit entfernen und auch freie Elemente mit Einfügung in konstanter Zeit zurückfordern / wiederverwenden / überschreiben. Um die Struktur zu durchlaufen, gehen Sie wie folgt vor:
for (int index = foos.first_node; index != -1; index = foos[index].next)
// do something with foos[index]
Und Sie können die obige Art der "verknüpften Anordnung von Löchern" -Datenstruktur mit Vorlagen verallgemeinern, neue und manuelle Dtor-Aufrufe platzieren, um die Notwendigkeit der Kopierzuweisung zu vermeiden, Destruktoren aufzurufen, wenn Elemente entfernt werden, einen Forward-Iterator bereitzustellen usw. I Ich habe mich dafür entschieden, das Beispiel sehr C-artig zu halten, um das Konzept klarer darzustellen und auch, weil ich sehr faul bin.
Das heißt, diese Struktur neigt dazu, sich in der räumlichen Lokalität zu verschlechtern, nachdem Sie Dinge zu / von der Mitte entfernt und viel eingefügt haben. Zu diesem Zeitpunkt können Sie über die next
Links entlang des Vektors vor- und zurückgehen und Daten, die zuvor aus einer Cache-Zeile innerhalb derselben sequenziellen Überquerung entfernt wurden, erneut laden (dies ist bei jeder Datenstruktur oder jedem Allokator unvermeidlich, der das Entfernen in konstanter Zeit ermöglicht, ohne Elemente beim Zurückfordern zu mischen Leerzeichen von der Mitte mit Einfügung in konstanter Zeit und ohne die Verwendung eines parallelen Bitsets oder eines removed
Flags). Um die Cache-Freundlichkeit wiederherzustellen, können Sie eine Kopier- und Auslagerungsmethode wie folgt implementieren:
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);
Jetzt ist die neue Version wieder Cache-freundlich zum Durchlaufen. Eine andere Methode besteht darin, eine separate Liste von Indizes in der Struktur zu speichern und diese regelmäßig zu sortieren. Eine andere Möglichkeit ist die Verwendung eines Bitsets, um anzugeben, welche Indizes verwendet werden. Dadurch durchlaufen Sie den Bit-Satz immer in sequentieller Reihenfolge (um dies effizient zu tun, prüfen Sie jeweils 64-Bit, z. B. mit FFS / FFZ). Das Bit-Set ist das effizienteste und nicht störendste, da nur ein paralleles Bit pro Element erforderlich ist, um anzugeben, welche verwendet und welche entfernt werden, anstatt 32-Bit- next
Indizes zu erfordern. Das Schreiben ist jedoch am zeitaufwändigsten (dies wird nicht der Fall sein) Seien Sie schnell beim Durchlaufen, wenn Sie jeweils ein Bit überprüfen. Sie müssen FFS / FFZ verwenden, um ein gesetztes oder nicht gesetztes Bit sofort unter 32+ Bits gleichzeitig zu finden, um die Bereiche der belegten Indizes schnell zu bestimmen.
Diese verknüpfte Lösung ist im Allgemeinen am einfachsten zu implementieren und nicht aufdringlich (Änderungen Foo
zum Speichern eines removed
Flags sind nicht erforderlich ). Dies ist hilfreich, wenn Sie diesen Container für die Arbeit mit einem beliebigen Datentyp verallgemeinern möchten, wenn Sie sich nicht um diese 32-Bit-Version kümmern Gemeinkosten pro Element.
Soll ich einen Speicherpool für die dynamische Zuweisung erstellen oder muss ich mich nicht darum kümmern? Was ist, wenn die Zielplattform mobile Geräte sind?
Need ist ein starkes Wort und ich arbeite voreingenommen in sehr leistungskritischen Bereichen wie Raytracing, Bildverarbeitung, Partikelsimulationen und Mesh-Verarbeitung, aber es ist relativ teuer, jugendliche Objekte zuzuweisen und freizugeben, die für eine sehr leichte Verarbeitung wie Aufzählungszeichen verwendet werden und Partikel einzeln gegen einen Allzweckspeicherzuordner mit variabler Größe. Da Sie in der Lage sein sollten, die oben genannte Datenstruktur in ein oder zwei Tagen zu verallgemeinern, um alles zu speichern, was Sie möchten, wäre es meiner Meinung nach ein lohnender Austausch, solche Kosten für die Heap-Zuweisung / -Deallocation direkt von der Bezahlung für jede einzelne Kleinigkeit zu streichen. Zusätzlich zur Reduzierung der Zuordnungs- / Freigabekosten erhalten Sie eine bessere Referenzlokalität bei der Überquerung der Ergebnisse (weniger Cachefehler und Seitenfehler).
Was Josh über GC anbelangt, habe ich die GC-Implementierung von C # nicht so genau untersucht wie die von Java, aber GC-Zuweiser haben häufig eine anfängliche ZuordnungDas ist sehr schnell, da hierfür ein sequentieller Allokator verwendet wird, der keinen Speicher in der Mitte freigibt (fast wie bei einem Stapel können Sie keine Objekte in der Mitte löschen). Dann zahlt es sich für die teuren Kosten aus, einzelne Objekte in einem separaten Thread tatsächlich entfernen zu können, indem der Speicher kopiert und der zuvor zugewiesene Speicher insgesamt gelöscht wird (z. B. den gesamten Stapel auf einmal zerstören, während die Daten in eine Art verknüpfte Struktur kopiert werden). Da dies jedoch in einem separaten Thread erfolgt, werden die Threads Ihrer Anwendung nicht unbedingt so stark blockiert. Dies birgt jedoch erhebliche versteckte Kosten für ein zusätzliches Indirektionsniveau und den allgemeinen Verlust des LOR nach einem anfänglichen GC-Zyklus. Es ist eine andere Strategie, um die Zuweisung zu beschleunigen - machen Sie es im aufrufenden Thread billiger und erledigen Sie dann die teure Arbeit in einem anderen. Dafür benötigen Sie zwei Indirektionsebenen, um auf Ihre Objekte zu verweisen, anstatt auf eine, da diese zwischen der ersten Zuweisung und einem ersten Zyklus im Speicher gemischt werden.
Eine andere Strategie in ähnlicher Weise, die in C ++ etwas einfacher anzuwenden ist, besteht darin, die Objekte in den Hauptthreads nicht freizugeben. Füge einfach weiter Daten hinzu und füge sie hinzu und füge sie am Ende einer Datenstruktur hinzu, die es nicht erlaubt, Dinge aus der Mitte zu entfernen. Markieren Sie jedoch die Dinge, die entfernt werden müssen. Dann könnte ein separater Thread die teure Arbeit erledigen, eine neue Datenstruktur ohne die entfernten Elemente zu erstellen und dann die neue atomar gegen die alte auszutauschen. Ein Großteil der Kosten für das Zuweisen und Freigeben von Elementen kann z Separater Thread, wenn Sie davon ausgehen können, dass die Anforderung zum Entfernen eines Elements nicht sofort erfüllt werden muss. Das macht das Freigeben nicht nur für Ihre Threads billiger, sondern auch die Zuweisung, da Sie eine viel einfachere und langwierigere Datenstruktur verwenden können, die niemals Fälle aus der Mitte entfernen muss. Es ist wie ein Container, der nur eine benötigtpush_back
Funktion zum Einfügen, eine clear
Funktion zum Entfernen aller Elemente und swap
zum Austauschen von Inhalten mit einem neuen, kompakten Container, der entfernte Elemente ausschließt; Das ist alles, was das Mutieren angeht.