Das dynamische Zuordnen und Trennen von Daten im laufenden Betrieb unabhängig von der Lebensdauer eines QT-Knotens, während die QT in Kombination mit der Kamera weiß, wann die Daten im laufenden Betrieb zugeordnet / getrennt werden sollten, ist etwas schwierig zu verallgemeinern, und ich denke, Ihre Lösung ist eigentlich nicht schlecht. Das ist eine schwierige Sache, gegen die man auf sehr schöne und allgemeine Weise vorgehen kann. wie "äh ... teste es gut und versende es!" Okay, ein kleiner Witz. Ich werde versuchen, einen Gedankengang zum Erkunden anzubieten. Eines der Dinge, die mich am meisten beeindruckten, war hier:
void nodeCreated(Node& node)
{
...
// One more thing, The QuadTree actually needs one field of
// Data to continue, so I fill it there
node.xxx = data.xxx
}
Dies sagt mir, dass ein Knotenreferenz / Zeiger nicht nur als Schlüssel für einen externen assoziativen Container verwendet wird. Sie greifen tatsächlich auf Interna des Quadtree-Knotens außerhalb des Quadtree selbst zu und ändern diese. Und es sollte einen ziemlich einfachen Weg geben, dies zumindest für den Anfang zu vermeiden. Wenn dies der einzige Ort ist, an dem Sie Knoteninternale außerhalb des Quadtree ändern, können Sie dies möglicherweise tun (sagen wir, es xxx
handelt sich um ein Paar Floats):
std::pair<float, float> nodeCreated(const Node& node)
{
Data data;
...
map[&node] = data;
...
return data.xxx;
}
An diesem Punkt kann der Quadtree den Rückgabewert dieser Funktion zum Zuweisen verwenden xxx
. Dadurch wird die Kopplung bereits erheblich gelockert, wenn Sie nicht mehr auf die Interna eines Baumknotens außerhalb des Baums zugreifen.
Wenn Sie nicht Terrain
mehr auf Quadtree-Interna zugreifen müssen, ist der einzige Ort, an dem Sie Dinge sehr unnötig koppeln, überflüssig. Dies ist die einzige echte PITA, wenn Sie beispielsweise Dinge mit einer GPU-Implementierung austauschen, da die GPU-Implementierung möglicherweise einen völlig anderen internen Repräsentanten für Knoten verwendet.
Aber für Ihre Leistungsbedenken, und da habe ich viel mehr Gedanken darüber, wie Sie mit solchen Dingen maximal eine Entkopplung erreichen, würde ich tatsächlich eine ganz andere Darstellung vorschlagen, bei der Sie die Datenzuordnung / -zuordnung in eine billige Operation mit konstanter Zeit verwandeln können. Es ist ein bisschen schwierig, jemandem zu erklären, der nicht daran gewöhnt ist, Standardcontainer zu erstellen, für die eine neue Platzierung erforderlich ist, um Elemente aus dem Poolspeicher zu erstellen. Daher beginne ich mit einigen Daten:
struct Node
{
....
// Stores an index to the data being associated on the fly
// or -1 if there's no data associated to the node.
int32_t data;
};
class Quadtree
{
private:
// Stores all the data being associated on the fly.
std::vector<char> data;
// Stores the size of the data being associated on the fly.
int32_t type_size;
// Stores an index to the first free index of data
// to reclaim or -1 if the free list is empty.
int32_t free_index;
...
public:
// Creates a quadtree with the specified type size for the
// data associated and disassociated on the fly.
explicit Quadtree(int32_t itype_size): type_size(itype_size), free_data(-1)
{
// Make sure our data type size is at least the size of an integer
// as required for the free list.
if (type_size < sizeof(int32_t))
type_size = sizeof(int32_t);
}
// Inserts a buffer to store a data element and returns an index
// to that.
int32_t alloc_data()
{
int32_t index = free_index;
if (free_index != -1)
{
// If a free index is available, pop it off the
// free list (stack) and return that.
void* mem = data.data() + index * type_size;
free_index = *static_cast<int*>mem;
}
else
{
// Otherwise insert the buffer for the data
// and return an index to that.
index = data.size() / type_size;
data.resize(data.size() + type_size);
}
return index;
}
// Frees the memory for the nth data element.
void free_data(int32_t n)
{
// Push the nth index to the free list to make
// it available for use in subsequent insertions.
void* mem = data.data() + n * type_size;
*static_cast<int*>(mem) = free_index;
free_index = n;
}
...
};
Das ist im Grunde eine "indizierte freie Liste". Wenn Sie diesen Mitarbeiter für die zugehörigen Daten verwenden, können Sie Folgendes tun:
class QTInterface
{
virtual std::pair<float, float> createData(void* mem) = 0;
virtual void destroyData(void* mem) = 0;
};
void Quadtree::update(Camera camera)
{
...
node.data = alloc_data();
node.xxx = i.createData(data.data() + node.data * type_size);
...
i.destroyData(data.data() + node.data * type_size);
free_data(node.data);
node.data = -1;
...
}
class Terrain : public QTInterface
{
// Note that we don't even need access to nodes anymore,
// not even as keys to use. We've completely decoupled
// terrains from tree internals.
std::pair<float, float> createData(void* mem) override
{
// Construct the data (placement new) using the memory
// allocated by the tree.
Data* data = new(mem) Data(...);
// Return data to assign to node.xxx.
return data->xxx;
}
void destroyData(void* mem) override
{
// Destroy the data.
static_cast<Data*>(mem)->~Data();
}
};
Hoffentlich macht dies alles Sinn, und natürlich ist es etwas entkoppelter von Ihrem ursprünglichen Design, da Clients keinen internen Zugriff auf Baumknotenfelder benötigen (es erfordert jetzt nicht einmal mehr Kenntnisse über Knoten, nicht einmal mehr als Schlüssel ), und es ist wesentlich effizienter, da Sie Daten in konstanter Zeit (und ohne Verwendung einer Hash-Tabelle, die eine viel größere Konstante implizieren würde) zu / von Knoten verknüpfen und trennen können. Ich hoffe, dass Ihre Daten mithilfe von max_align_t
(z. B. ohne SIMD-Felder) ausgerichtet werden können und trivial kopierbar sind. Andernfalls werden die Dinge erheblich komplexer, da wir einen ausgerichteten Allokator benötigen und möglicherweise unseren eigenen kostenlosen Listencontainer rollen müssen. Nun, wenn Sie nur nicht trivial kopierbare Typen haben und nicht mehr als benötigenmax_align_t
können wir eine kostenlose Listenzeigerimplementierung verwenden, die nicht gerollte Knoten, die jeweils K
Datenelemente speichern, bündelt und verknüpft , um zu vermeiden, dass vorhandene Speicherblöcke neu zugewiesen werden müssen. Ich kann das zeigen, wenn Sie eine solche Alternative brauchen.
Es ist etwas fortgeschritten und sehr C ++ - spezifisch, da die Idee, Speicher für Elemente zuzuweisen und freizugeben, eine separate Aufgabe ist, die vom Erstellen und Zerstören dieser Elemente getrennt ist. Wenn Sie dies jedoch auf diese Weise tun, übernehmen Sie Terrain
die Mindestverantwortung und erfordern keinerlei interne Kenntnisse der Baumdarstellung mehr, auch nicht für undurchsichtige Knoten. Diese Ebene der Speichersteuerung ist jedoch in der Regel das, was Sie benötigen, um die effizientesten Datenstrukturen zu entwerfen.
Die Grundidee dort ist, dass der Client den Tree-Pass in der Typgröße der Daten verwendet, die er im laufenden Betrieb dem Quadtree-Ctor zuordnen / trennen möchte. Dann hat der Quadtree die Verantwortung, Speicher mit dieser Typgröße zuzuweisen und freizugeben. Anschließend wird die Verantwortung für die Erstellung und Zerstörung der Daten mithilfe eines QTInterface
dynamischen Versands an den Client weitergegeben . Die einzige Verantwortung außerhalb des Baums, der noch mit dem Baum zusammenhängt, besteht daher darin, Elemente aus dem Speicher zu konstruieren und zu zerstören, die der Quadtree selbst zuweist und aufhebt. An diesem Punkt werden die Abhängigkeiten wie folgt:
Was angesichts der Schwierigkeit Ihrer Arbeit und der Größe der Eingaben sehr vernünftig ist. Grundsätzlich Terrain
hängt Ihr dann nur noch von Quadtree
und ab QTInterface
und nicht mehr von den Interna des Quadtree oder seiner Knoten. Zuvor hatten Sie Folgendes:
Und natürlich ist ein eklatantes Problem dabei, insbesondere wenn Sie GPU-Implementierungen ausprobieren möchten , diese Abhängigkeit von Terrain
bis Node
, da eine GPU-Implementierung wahrscheinlich einen ganz anderen Knotenvertreter verwenden möchte. Wenn du Hardcore SOLID machen willst, machst du natürlich so etwas:
... zusammen mit möglicherweise einer Fabrik. Aber IMO ist ein völliger Overkill (zumindest INode
ist es ein totaler Overkill IMO) und wäre in einem so granularen Fall wie einer Quadtree-Funktion nicht sehr hilfreich, wenn jeder einen dynamischen Versand erfordern würde.
Es fällt mir immer schwer, meine Klassen richtig zu entkoppeln. Irgendwelche Ratschläge, die ich später verwenden könnte? (Wie zum Beispiel, welche Fragen muss ich mir stellen oder wie verarbeitest du? Wenn ich auf dem Papier darüber nachdenke, erscheint mir das sehr abstrakt, und wenn ich sofort etwas codiere, führt dies zu einem späteren Refactoring.)
Ganz allgemein und grob gesagt läuft die Entkopplung oft darauf hinaus, die Menge an Informationen zu begrenzen, die eine bestimmte Klasse oder Funktion über etwas anderes benötigt, um ihre Sache zu tun.
Ich gehe davon aus, dass Sie C ++ verwenden, da keine andere mir bekannte Sprache genau diese Syntax hat. In C ++ sind Klassenvorlagen mit statischem Polymorphismus ein sehr effektiver Entkopplungsmechanismus für Datenstrukturen, wenn Sie sie verwenden können. Wenn Sie die Standardcontainer wie betrachten std::vector<T, Alloc>
, ist der Vektor nicht an das gekoppelt, was Sie für T
irgendetwas angegeben haben. Es ist nur erforderlich, dass T
einige grundlegende Schnittstellenanforderungen erfüllt werden, z. B. dass es kopierkonstruierbar ist und einen Standardkonstruktor für den Füllkonstruktor und die Größenänderung der Füllung hat. Und es werden niemals Änderungen aufgrund von T
Änderungen erforderlich sein .
Wenn Sie es mit dem oben Gesagten verknüpfen, können Sie die Datenstruktur mit dem absolut minimalen Wissen über das, was sie enthält, implementieren, und das entkoppelt sie in dem Maße, in dem sie im Voraus nicht einmal Typinformationen benötigt (hier ist der Fortschritt) Sprechen in Bezug auf Codeabhängigkeiten / Kopplung, nicht Informationen zur Kompilierungszeit) darüber, was T
ist.
Der zweitpraktischste Weg, um die erforderliche Informationsmenge zu minimieren, ist die Verwendung eines dynamischen Polymorphismus. Wenn Sie beispielsweise eine einigermaßen verallgemeinerte Datenstruktur implementieren möchten, die das Wissen darüber, was gespeichert wird, minimiert, können Sie die Schnittstellenanforderungen für das, was in einer oder mehreren Schnittstellen gespeichert ist, erfassen:
// Contains all the functions (pure virtual) required of the elements
// stored in the container.
class IElement {...};
In beiden Fällen geht es jedoch darum, die Menge an Informationen, die Sie im Voraus benötigen, zu minimieren, indem Sie eher auf Schnittstellen als auf konkrete Details codieren. Hier ist die einzige große Sache, die Sie tun, die viel mehr Informationen als erforderlich zu erfordern scheint, dass Sie Terrain
vollständige Informationen über die Interna eines Quadtree-Knotens haben müssen, z. B. in einem solchen Fall, vorausgesetzt, der einzige Grund, den Sie benötigen, ist Um einem Knoten ein Datenelement zuzuweisen, können wir diese Abhängigkeit von den Interna eines Baumknotens leicht beseitigen, indem wir lediglich die Daten zurückgeben, die dem Knoten in dieser Zusammenfassung zugewiesen werden sollen QTInterface
.
Wenn ich also etwas entkoppeln möchte, konzentriere ich mich nur auf das, was es braucht, um es zu tun, und erstelle eine Schnittstelle dafür (entweder explizit unter Verwendung von Vererbung oder implizit unter Verwendung von statischem Polymorphismus und Ententypisierung). Und Sie haben dies bereits zu einem gewissen Grad aus dem Quadtree selbst heraus getan, um QTInterface
es dem Client zu ermöglichen, seine Funktionen mit einem Subtyp zu überschreiben und die konkreten Details bereitzustellen, die erforderlich sind, damit der Quadtree seine Aufgabe erfüllt. Der einzige Ort, an dem Sie meiner Meinung nach zu kurz gekommen sind, ist, dass der Kunde weiterhin Zugriff auf die Interna des Quadtree benötigt. Sie können dies vermeiden, indem Sie erhöhen, was QTInterface
genau das ist, was ich vorgeschlagen habe, als ich einen Wert zurückgab, dem zugewiesen werden sollnode.xxx
in der quadtree Implementierung selbst. Es geht also nur darum, die Dinge abstrakter und die Schnittstellen vollständiger zu gestalten, damit die Dinge keine unnötigen Informationen über einander erfordern.
Und indem Sie diese unnötigen Informationen vermeiden (Sie Terrain
müssen sich mit Quadtree
Knoteninternalen auskennen), können Sie diese jetzt Quadtree
beispielsweise durch eine GPU-Implementierung austauschen, ohne auch die Terrain
Implementierung zu ändern . Was Dinge nicht voneinander wissen, kann sich ändern, ohne sich gegenseitig zu beeinflussen. Wenn Sie wirklich GPU-Quadtree-Implementierungen von CPU-Implementierungen austauschen möchten, gehen Sie möglicherweise ein wenig in Richtung der obigen SOLID-Route mitIQuadtree
(Den Quadtree selbst abstrakt machen). Dies ist mit einem dynamischen Versandtreffer verbunden, der mit der Baumtiefe und den Eingabegrößen, über die Sie sprechen, etwas teuer werden kann. Wenn nicht, sind zumindest weitaus weniger Änderungen am Code erforderlich, wenn die Dinge, die den Quadtree verwenden, nicht über die interne Knotendarstellung Bescheid wissen müssen, um zu funktionieren. Möglicherweise können Sie eine Datei mit der anderen austauschen, indem Sie nur eine einzelne Codezeile für a aktualisieren typedef
, z. B. auch wenn Sie keine abstrakte Schnittstelle verwenden ( IQuadtree
).
Aber da habe ich wohl mein erstes Problem. Die meiste Zeit mache ich mir keine Gedanken über die Optimierung, bis ich sie sehe, aber ich denke, wenn ich diese Art von Overhead hinzufügen muss, um meine Klassen richtig zu entkoppeln, liegt das daran, dass das Design Fehler aufweist.
Nicht unbedingt. Entkopplung bedeutet oft, eine Abhängigkeit vom Konkreten zum Abstrakten zu verschieben. Abstraktionen bedeuten in der Regel eine Laufzeitstrafe, es sei denn, der Compiler generiert zur Kompilierungszeit Code, um die Abstraktionskosten zur Laufzeit zu eliminieren. Im Gegenzug erhalten Sie viel mehr Spielraum, um Änderungen vorzunehmen, ohne andere Dinge zu beeinflussen. Dies führt jedoch häufig zu Leistungseinbußen, es sei denn, Sie verwenden die Codegenerierung.
Jetzt können Sie die Notwendigkeit einer nicht trivialen assoziativen Datenstruktur (Karte / Wörterbuch, dh) beseitigen, um Daten im laufenden Betrieb Knoten (oder etwas anderem) zuzuordnen. Im obigen Fall habe ich gerade die Knoten dazu gebracht, direkt einen Index für die Daten zu speichern, die im laufenden Betrieb zugewiesen / freigegeben werden. Diese Art von Dingen zu tun, hängt weniger mit dem Studium zusammen, wie man Dinge effektiv entkoppelt, als vielmehr damit, wie man Speicherlayouts für Datenstrukturen effektiv nutzt (mehr im Bereich der reinen Optimierung).
Effektive SE-Prinzipien und -Leistungen stehen auf ausreichend niedrigen Niveaus im Widerspruch zueinander. Häufig werden durch die Entkopplung die Speicherlayouts für Felder, auf die häufig gemeinsam zugegriffen wird, aufgeteilt, es können mehr Heap-Zuweisungen erforderlich sein, es kann sich um einen dynamischeren Versand handeln usw. Sie werden schnell trivialisiert, wenn Sie auf Code höherer Ebene hinarbeiten (z. B. Operationen, die auf ganze Bilder angewendet werden, nicht per -Pixeloperationen beim Durchlaufen einzelner Pixel), aber die Kosten reichen von trivial bis schwerwiegend, je nachdem, wie viel diese Kosten in Ihrem kritischsten, schleifenförmigen Code anfallen, der die leichteste Arbeit in jeder Iteration ausführt.
Überkompliziere ich Dinge? Hätte ich die Node-Klasse nur erweitern sollen, damit sie zu einer Tüte mit Daten wird, die von einigen Klassen verwendet werden?
Ich persönlich finde das nicht so schlimm, wenn Sie nicht versuchen, Ihre Datenstruktur zu stark zu verallgemeinern, sondern sie nur in einem sehr begrenzten Kontext verwenden und sich mit einem äußerst leistungskritischen Kontext für eine Art von Problem befassen, das Sie haben noch nicht in Angriff genommen. In diesem Fall würden Sie Ihren Quadtree in ein Implementierungsdetail Ihres Terrains verwandeln, z. B. in etwas, das weit verbreitet und öffentlich verwendet werden soll. Auf ähnliche Weise könnte jemand ein Octree in ein Implementierungsdetail seiner Physik-Engine verwandeln, indem er das nicht mehr unterscheidet Idee der "öffentlichen Schnittstelle" von "Interna". Das Verwalten von Invarianten in Bezug auf den räumlichen Index wird dann zur Verantwortung der Klasse, die ihn als privates Implementierungsdetail verwendet.
Um eine effektive Abstraktion (dh Schnittstelle) in einem leistungskritischen Kontext zu entwerfen, müssen Sie häufig den Großteil des Problems gründlich verstehen und eine sehr effektive Lösung dafür im Voraus finden. Es kann sich tatsächlich in eine kontraproduktive Maßnahme verwandeln, um zu versuchen, die Lösung zu verallgemeinern und zu abstrahieren, während gleichzeitig versucht wird, das effektive Design über mehrere Iterationen herauszufinden. Einer der Gründe ist, dass leistungskritische Kontexte sehr effiziente Datendarstellungen und Zugriffsmuster erfordern. Abstraktionen stellen eine Barriere zwischen dem Code dar, der auf die Daten zugreifen möchte: Eine Barriere, die nützlich ist, wenn Sie möchten, dass sich die Daten frei ändern können, ohne diesen Code zu beeinflussen, aber ein Hindernis, wenn Sie gleichzeitig versuchen, die effektivste Art der Darstellung herauszufinden und in erster Linie auf solche Daten zugreifen.
Aber wenn Sie es so machen, würde ich mich wieder irren, wenn ich den Quadtree in ein privates Implementierungsdetail Ihres Terrains verwandeln würde, das nicht außerhalb ihrer Implementierungen verallgemeinert und verwendet werden sollte. Und Sie müssten auf die Idee verzichten, GPU-Implementierungen so einfach von CPU-Implementierungen austauschen zu können, da dies normalerweise eine Abstraktion erfordern würde, die für beide funktioniert und nicht direkt von den konkreten Details abhängt (wie Knotenwiederholungen). von beiden.
Der Punkt der Entkopplung
Aber vielleicht ist dies in einigen Fällen sogar für öffentlich genutzte Dinge akzeptabel. Bevor die Leute denken, dass ich verrückten Unsinn ausspucke, sollten Sie Bildschnittstellen in Betracht ziehen. Wie viele davon würden für einen Videoprozessor ausreichen, der Bildfilter in Echtzeit auf das Video anwenden muss, wenn das Bild seine Interna nicht verfügbar macht (direkter Zugriff auf das zugrunde liegende Pixelarray in einem bestimmten Pixelformat)? Es gibt keine, von denen ich weiß, dass sie getPixel
hier und da so etwas wie abstrakt / virtuell verwendensetPixel
Dort werden Pixelformatkonvertierungen pro Pixel durchgeführt. In ausreichend leistungskritischen Kontexten, in denen Sie auf sehr detaillierte Ebenen zugreifen müssen (pro Pixel, pro Knoten usw.), müssen Sie möglicherweise manchmal die Interna der zugrunde liegenden Struktur verfügbar machen. Aber zwangsläufig müssen Sie die Dinge zwangsläufig eng miteinander verbinden, und es wird dann nicht einfach sein, die zugrunde liegende Darstellung von Bildern (z. B. Änderung des Bildformats) sozusagen zu ändern, ohne dass sich dies auf alles auswirkt, was auf die zugrunde liegenden Pixel zugreift. In diesem Fall kann es jedoch weniger Gründe für Änderungen geben, da es möglicherweise einfacher ist, die Datendarstellung zu stabilisieren als die abstrakte Schnittstelle. Ein Videoprozessor könnte sich möglicherweise auf die Idee einigen, 32-Bit-RGBA-Pixelformate zu verwenden, und diese Entwurfsentscheidung könnte sich über Jahre hinweg nicht ändern.
Idealerweise möchten Sie, dass die Abhängigkeiten in Richtung Stabilität fließen (unveränderliche Dinge), da das Ändern von Elementen mit vielen Abhängigkeiten die Kosten mit der Anzahl der Abhängigkeiten vervielfacht. Das können in allen Fällen Abstraktionen sein oder auch nicht. Das ignoriert natürlich die Vorteile von Informationen, die sich bei der Aufrechterhaltung von Invarianten verstecken, aber unter dem Gesichtspunkt der Kopplung besteht der Hauptpunkt der Entkopplung darin, die Änderungen kostengünstiger zu gestalten. Das bedeutet, Abhängigkeiten von Dingen, die sich ändern könnten, auf Dinge umzuleiten, die sich nicht ändern, und das hilft nicht im geringsten, wenn Ihre abstrakten Schnittstellen die sich am schnellsten ändernden Teile Ihrer Datenstruktur sind.
Wenn Sie dies aus Kopplungssicht zumindest geringfügig verbessern möchten, trennen Sie Ihre Knotenteile, auf die Clients zugreifen müssen, von Teilen, die dies nicht tun. Ich gehe davon aus, dass Clients zumindest die Links des Knotens nicht aktualisieren müssen, sodass die Links nicht verfügbar gemacht werden müssen. Sie sollten zumindest in der Lage sein, ein Wertaggregat zu erstellen, das von der Gesamtheit der Knoten, auf die Clients zugreifen / die sie ändern können, getrennt ist NodeValue
.