Mi pregunta es, dado que no estoy iterando linealmente una matriz contigua a la vez en estos casos, ¿estoy sacrificando de inmediato las ganancias de rendimiento de la asignación de componentes de esta manera?
Lo más probable es que obtenga menos pérdidas de caché en general con matrices "verticales" separadas por tipo de componente que intercalar los componentes unidos a una entidad en un bloque de tamaño variable "horizontal", por así decirlo.
La razón es porque, primero, la representación "vertical" tenderá a usar menos memoria. No tiene que preocuparse por la alineación de matrices homogéneas asignadas contiguamente. Con los tipos no homogéneos asignados a un grupo de memoria, debe preocuparse por la alineación, ya que el primer elemento de la matriz podría tener un tamaño y requisitos de alineación totalmente diferentes del segundo. Como resultado, a menudo necesitará agregar relleno, como un simple ejemplo:
// 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;
};
Digamos que queremos intercalar Foo
y Bar
almacenarlos uno al lado del otro en la 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;
};
Ahora, en lugar de tomar 18 bytes para almacenar Foo y Bar en regiones de memoria separadas, se necesitan 24 bytes para fusionarlos. No importa si cambias el orden:
// 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'
};
Si toma más memoria en un contexto de acceso secuencial sin mejorar significativamente los patrones de acceso, generalmente incurrirá en más errores de caché. Además de eso, el paso para pasar de una entidad a la siguiente aumenta y a un tamaño variable, lo que hace que tenga que dar saltos de tamaño variable en la memoria para pasar de una entidad a la siguiente solo para ver cuáles tienen los componentes que usted ' Estás interesado en
Por lo tanto, usar una representación "vertical" como lo hace para almacenar tipos de componentes es en realidad más probable que sea óptima que las alternativas "horizontales". Dicho esto, el problema con los errores de caché con la representación vertical se puede ejemplificar aquí:
Donde las flechas simplemente indican que la entidad "posee" un componente. Podemos ver que si tratamos de acceder a todos los componentes de movimiento y renderizado de entidades que tienen ambos, terminaríamos saltando por todos lados en la memoria. Ese tipo de patrón de acceso esporádico puede hacer que cargue datos en una línea de caché para acceder, por ejemplo, a un componente de movimiento, luego acceder a más componentes y desalojar esos datos anteriores, solo para cargar nuevamente la misma región de memoria que ya fue desalojada por otro movimiento componente. Por lo tanto, puede ser un desperdicio cargar exactamente las mismas regiones de memoria más de una vez en una línea de caché solo para recorrer y acceder a una lista de componentes.
Vamos a limpiar ese desastre un poco para que podamos ver más claramente:
Tenga en cuenta que si encuentra este tipo de escenario, generalmente es mucho después de que el juego ha comenzado a ejecutarse, después de que se hayan agregado y eliminado muchos componentes y entidades. En general, cuando comienza el juego, puede agregar todas las entidades y componentes relevantes juntos, en ese momento pueden tener un patrón de acceso secuencial muy ordenado con buena ubicación espacial. Sin embargo, después de muchas extracciones e inserciones, es posible que termines obteniendo algo como el desastre anterior.
Una manera muy fácil de mejorar esa situación es simplemente ordenar por radix sus componentes en función del ID / índice de la entidad que los posee. En ese momento obtienes algo como esto:
Y ese es un patrón de acceso mucho más amigable con la caché. No es perfecto ya que podemos ver que tenemos que omitir algunos componentes de renderizado y movimiento aquí y allá ya que nuestro sistema solo está interesado en entidades que tienen ambos , y algunas entidades solo tienen un componente de movimiento y algunas solo tienen un componente de renderizado. , pero al menos puede procesar algunos componentes contiguos (más en la práctica, por lo general, ya que a menudo adjuntará componentes relevantes de interés, como quizás más entidades en su sistema que tienen un componente de movimiento tendrán un componente de representación que no).
Lo más importante, una vez que los haya ordenado, no cargará datos en una región de memoria en una línea de caché solo para luego volver a cargarlos en un solo bucle.
Y esto no requiere un diseño extremadamente complejo, solo un pase de clasificación de radix de tiempo lineal de vez en cuando, tal vez después de que haya insertado y eliminado un grupo de componentes para un tipo de componente en particular, en ese momento puede marcarlo como necesita ser ordenado Una clasificación de radix implementada razonablemente (incluso puede paralelizarla, lo que hago) puede ordenar un millón de elementos en aproximadamente 6 ms en mi i7 quad-core, como se ejemplifica aquí:
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 ]
Lo anterior es ordenar un millón de elementos 32 veces (incluido el tiempo de memcpy
resultados antes y después de la clasificación). Y supongo que la mayoría de las veces en realidad no tendrá más de un millón de componentes para clasificar, por lo que debería poder colarse fácilmente aquí y allá sin causar ningún tartamudeo notable de la velocidad de fotogramas.