Meine Frage ist, da ich in diesen Fällen nicht linear jeweils ein zusammenhängendes Array iteriere, opfere ich sofort die Leistungsverbesserungen, die durch die Zuweisung von Komponenten auf diese Weise erzielt werden?
Es besteht die Möglichkeit, dass Sie mit separaten "vertikalen" Arrays pro Komponententyp insgesamt weniger Cache-Ausfälle erhalten, als wenn Sie die an eine Entität angehängten Komponenten sozusagen in einem "horizontalen" Block mit variabler Größe verschachteln.
Der Grund dafür ist, dass erstens die "vertikale" Darstellung dazu neigt, weniger Speicher zu verwenden. Sie müssen sich nicht um die Ausrichtung von zusammenhängend zugewiesenen homogenen Arrays kümmern. Bei inhomogenen Typen, die einem Speicherpool zugeordnet sind, müssen Sie sich um die Ausrichtung kümmern, da das erste Element im Array möglicherweise andere Größen- und Ausrichtungsanforderungen als das zweite hat. Infolgedessen müssen Sie häufig Auffüllungen hinzufügen, wie zum Beispiel:
// Assuming 8-bit chars and 64-bit doubles.
struct Foo
{
// 1 byte
char a;
// 1 byte
char b;
};
struct Bar
{
// 8 bytes
double opacity;
// 8 bytes
double radius;
};
Nehmen wir an, wir möchten sie verschachteln Foo
und Bar
direkt nebeneinander speichern:
// Assuming 8-bit chars and 64-bit doubles.
struct FooBar
{
// 1 byte
char a;
// 1 byte
char b;
// 6 bytes padding for 64-bit alignment of 'opacity'
// 8 bytes
double opacity;
// 8 bytes
double radius;
};
Anstatt nun 18 Bytes zu benötigen, um Foo und Bar in separaten Speicherbereichen zu speichern, sind 24 Bytes erforderlich, um sie zu verschmelzen. Es spielt keine Rolle, ob Sie die Bestellung tauschen:
// Assuming 8-bit chars and 64-bit doubles.
struct BarFoo
{
// 8 bytes
double opacity;
// 8 bytes
double radius;
// 1 byte
char a;
// 1 byte
char b;
// 6 bytes padding for 64-bit alignment of 'opacity'
};
Wenn Sie in einem Kontext mit sequenziellem Zugriff mehr Speicher beanspruchen, ohne die Zugriffsmuster wesentlich zu verbessern, treten in der Regel mehr Cache-Fehler auf. Darüber hinaus nimmt der Schritt von einer Entität zur nächsten und zu einer variablen Größe zu, sodass Sie einen Sprung in den Speicher machen müssen, um von einer Entität zur nächsten zu gelangen, nur um zu sehen, welche die von Ihnen verwendeten Komponenten enthalten. ' Ich bin interessiert an.
Die Verwendung einer "vertikalen" Darstellung zum Speichern von Komponententypen ist daher mit größerer Wahrscheinlichkeit optimal als "horizontale" Alternativen. Das Problem mit Cache-Fehlern bei der vertikalen Darstellung kann hier beispielhaft dargestellt werden:
Wo die Pfeile einfach anzeigen, dass die Entität eine Komponente "besitzt". Wir können sehen, dass wir, wenn wir versuchen, auf alle Bewegungs- und Renderkomponenten von Entitäten zuzugreifen, die beides enthalten, am Ende überall im Gedächtnis herumspringen. Bei dieser Art von sporadischem Zugriffsmuster können Sie Daten in eine Cache-Zeile laden, um beispielsweise auf eine Bewegungskomponente zuzugreifen, dann auf mehrere Komponenten zuzugreifen und diese früheren Daten zu entfernen, um dann denselben Speicherbereich erneut zu laden, der bereits für eine andere Bewegung entfernt wurde Komponente. Das kann also sehr verschwenderisch sein, wenn genau dieselben Speicherbereiche mehr als einmal in eine Cache-Zeile geladen werden, nur um eine Liste von Komponenten zu durchlaufen und darauf zuzugreifen.
Räumen wir das Chaos ein wenig auf, damit wir klarer sehen können:
Beachten Sie, dass es in der Regel lange nach dem Start des Spiels dauert, bis viele Komponenten und Entitäten hinzugefügt und entfernt wurden, wenn Sie auf ein solches Szenario stoßen. Im Allgemeinen können Sie zu Beginn des Spiels alle Entitäten und relevanten Komponenten zusammenfassen. Zu diesem Zeitpunkt verfügen sie möglicherweise über ein sehr geordnetes, sequenzielles Zugriffsmuster mit guter räumlicher Lokalität. Nach vielen Umzügen und Einfügungen kann es jedoch vorkommen, dass Sie so etwas wie das obige Chaos bekommen.
Eine sehr einfache Möglichkeit, diese Situation zu verbessern, besteht darin, Ihre Komponenten einfach nach der Entitäts-ID / dem Index zu sortieren, deren Eigentümer sie sind. An diesem Punkt erhalten Sie so etwas:
Und das ist ein viel Cache-freundlicheres Zugriffsmuster. Es ist nicht perfekt, da wir sehen, dass wir hier und da einige Rendering- und Bewegungskomponenten überspringen müssen, da unser System nur an Entitäten interessiert ist, die beide haben, und einige Entitäten nur eine Bewegungskomponente und einige nur eine Rendering-Komponente haben Sie sind jedoch letztendlich in der Lage, einige zusammenhängende Komponenten zu verarbeiten (in der Praxis ist dies in der Regel der Fall, da Sie häufig relevante Komponenten hinzufügen, z. B., dass mehr Entitäten in Ihrem System, die über eine Bewegungskomponente verfügen, über eine Renderkomponente verfügen als nicht).
Am wichtigsten ist, dass Sie nach dem Sortieren der Daten keinen Speicherbereich mehr in eine Cache-Zeile laden, um sie dann in einer einzigen Schleife neu zu laden.
Und dies erfordert kein extrem komplexes Design, nur hin und wieder einen Radix-Sortierdurchlauf in linearer Zeit, möglicherweise nachdem Sie eine Reihe von Komponenten für einen bestimmten Komponententyp eingefügt und entfernt haben. An diesem Punkt können Sie sie als markieren sortiert werden müssen. Eine vernünftig implementierte Radix-Sortierung (Sie können sie sogar parallelisieren, was ich auch tue) kann eine Million Elemente in ungefähr 6 ms auf meinem Quad-Core i7 sortieren, wie hier gezeigt:
Sorting 1000000 elements 32 times...
mt_sort_int: {0.203000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_sort: {1.248000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_radix_sort: {0.202000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
std::sort: {1.810000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
qsort: {2.777000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
Oben wird eine Million Elemente 32-mal sortiert (einschließlich der Zeit bis zu den memcpy
Ergebnissen vor und nach dem Sortieren). Und ich gehe davon aus, dass Sie die meiste Zeit nicht wirklich über eine Million Komponenten sortieren müssen. Deshalb sollten Sie dies hier und da problemlos tun können, ohne dass es zu merklichen Bildstörungen kommt.