La mia domanda è, poiché in questi casi non sto ripetendo linearmente un array contiguo alla volta, sto sacrificando immediatamente i guadagni in termini di prestazioni allocando i componenti in questo modo?
È probabile che nel complesso si verifichino meno errori cache con matrici "verticali" separate per tipo di componente rispetto all'interlacciamento dei componenti collegati a un'entità in un blocco di dimensioni variabili "orizzontale", per così dire.
Il motivo è perché, in primo luogo, la rappresentazione "verticale" tenderà a utilizzare meno memoria. Non devi preoccuparti dell'allineamento per array omogenei assegnati in modo contiguo. Con i tipi non omogenei allocati in un pool di memoria, è necessario preoccuparsi dell'allineamento poiché il primo elemento dell'array potrebbe avere dimensioni e requisiti di allineamento completamente diversi dal secondo. Di conseguenza dovrai spesso aggiungere imbottitura, come ad esempio un semplice esempio:
// 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;
};
Diciamo che vogliamo intercalare Foo
e Bar
archiviarli uno accanto all'altro in memoria:
// 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;
};
Ora invece di prendere 18 byte per memorizzare Foo e Bar in aree di memoria separate, ci vogliono 24 byte per fonderli. Non importa se si scambia l'ordine:
// 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'
};
Se si prende più memoria in un contesto di accesso sequenziale senza migliorare in modo significativo i modelli di accesso, generalmente si verificheranno più perdite di cache. Inoltre, il passo da passare da un'entità alla successiva aumenta e ad una dimensione variabile, costringendoti a fare salti di dimensioni variabili in memoria per passare da un'entità all'altra solo per vedere quali hanno i componenti che " sei interessato a.
Pertanto, l'utilizzo di una rappresentazione "verticale" durante la memorizzazione dei tipi di componenti ha effettivamente maggiori probabilità di essere ottimale rispetto alle alternative "orizzontali". Detto questo, il problema con la cache manca con la rappresentazione verticale può essere esemplificato qui:
Dove le frecce indicano semplicemente che l'entità "possiede" un componente. Possiamo vedere che se dovessimo provare ad accedere a tutti i componenti di movimento e rendering delle entità che hanno entrambi, finiremmo per saltare dappertutto nella memoria. Quel tipo di modello di accesso sporadico può farti caricare i dati in una riga della cache per accedere, diciamo, a un componente di movimento, quindi accedere a più componenti e far sfrattare quei dati precedenti, solo per caricare nuovamente la stessa area di memoria che è stata già sfrattata per un altro movimento componente. Quindi può essere molto dispendioso caricare le stesse identiche aree di memoria più di una volta in una riga della cache solo per scorrere e accedere a un elenco di componenti.
Puliamo un po 'quel casino in modo da poter vedere più chiaramente:
Nota che se incontri questo tipo di scenario, di solito è molto tempo dopo l'inizio del gioco, dopo che molti componenti ed entità sono stati aggiunti e rimossi. In generale, quando il gioco inizia, potresti aggiungere tutte le entità e i componenti rilevanti insieme, a quel punto potrebbero avere un modello di accesso sequenziale molto ordinato con una buona località spaziale. Dopo un sacco di rimozioni e inserimenti, potresti finire per ottenere qualcosa come il pasticcio sopra.
Un modo molto semplice per migliorare quella situazione è semplicemente ordinare i componenti in modo radicale in base all'ID / indice dell'entità che li possiede. A quel punto ottieni qualcosa del genere:
E questo è un modello di accesso molto più intuitivo. Non è perfetto poiché possiamo vedere che dobbiamo saltare alcuni componenti di rendering e di movimento qua e là poiché il nostro sistema è interessato solo a entità che hanno entrambi , e alcune entità hanno solo una componente di movimento e alcune hanno solo una componente di rendering , ma almeno finisci per essere in grado di elaborare alcuni componenti contigui (più nella pratica, in genere, poiché spesso collegherai componenti rilevanti di interesse, come forse più entità nel tuo sistema che hanno un componente di movimento avranno un componente di rendering di non).
Ancora più importante, una volta ordinati questi, non caricherai i dati di una regione di memoria in una riga della cache solo per ricaricarli in un singolo ciclo.
E questo non richiede un design estremamente complesso, solo un radix-time lineare passa ogni tanto, forse dopo aver inserito e rimosso un gruppo di componenti per un particolare tipo di componente, a quel punto puoi contrassegnarlo come deve essere ordinato. Un ordinamento radix ragionevolmente implementato (puoi persino parallelizzarlo, cosa che faccio) può ordinare un milione di elementi in circa 6ms sul mio quad-core i7, come esemplificato qui:
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 ]
Quanto sopra è di ordinare un milione di elementi 32 volte (incluso il tempo necessario per i memcpy
risultati prima e dopo l'ordinamento). E suppongo che la maggior parte delle volte non avrai effettivamente più di un milione di componenti da ordinare, quindi dovresti essere molto facilmente in grado di intrufolarlo di tanto in tanto senza causare alcun evidente scatti alla frequenza dei fotogrammi.