Ma question est, puisque je n'itère pas de manière linéaire un tableau contigu à la fois dans ces cas, est-ce que je sacrifie immédiatement les gains de performances de l'allocation de composants de cette façon?
Les chances sont que vous obtiendrez globalement moins de ratés de cache avec des tableaux "verticaux" séparés par type de composant que l'entrelacement des composants attachés à une entité dans un bloc de taille variable "horizontal", pour ainsi dire.
La raison en est que, premièrement, la représentation "verticale" aura tendance à utiliser moins de mémoire. Vous n'avez pas à vous soucier de l'alignement pour les tableaux homogènes alloués de manière contiguë. Avec des types non homogènes alloués dans un pool de mémoire, vous devez vous soucier de l'alignement car le premier élément du tableau peut avoir une taille et des exigences d'alignement totalement différentes du second. En conséquence, vous devrez souvent ajouter un rembourrage, comme dans un exemple simple:
// 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;
};
Disons que nous voulons les entrelacer Foo
et les Bar
stocker les uns à côté des autres en mémoire:
// 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;
};
Maintenant, au lieu de prendre 18 octets pour stocker Foo et Bar dans des régions de mémoire distinctes, il faut 24 octets pour les fusionner. Peu importe si vous échangez la commande:
// 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 vous prenez plus de mémoire dans un contexte d'accès séquentiel sans améliorer de manière significative les modèles d'accès, vous aurez généralement plus de ratés de cache. En plus de cela, la foulée pour passer d'une entité à la suivante augmente et à une taille variable, ce qui vous oblige à faire des sauts de taille variable en mémoire pour passer d'une entité à la suivante juste pour voir lesquels ont les composants que vous '' suis intéressé par.
Ainsi, l'utilisation d'une représentation "verticale" comme vous le faites pour le stockage des types de composants est en fait plus susceptible d'être optimale que les alternatives "horizontales". Cela dit, le problème des échecs de cache avec la représentation verticale peut être illustré ici:
Où les flèches indiquent simplement que l'entité "possède" un composant. Nous pouvons voir que si nous essayions d'accéder à tous les composants de mouvement et de rendu d'entités qui ont les deux, nous finissons par sauter partout dans la mémoire. Ce type de modèle d'accès sporadique peut vous amener à charger des données dans une ligne de cache pour accéder, par exemple, à un composant de mouvement, puis accéder à plus de composants et faire expulser ces anciennes données, uniquement pour charger à nouveau la même région de mémoire qui a déjà été expulsée pour un autre mouvement. composant. Cela peut donc être très inutile de charger les mêmes régions de mémoire plus d'une fois dans une ligne de cache juste pour parcourir et accéder à une liste de composants.
Nettoyons un peu ce gâchis pour que nous puissions voir plus clairement:
Notez que si vous rencontrez ce type de scénario, c'est généralement longtemps après le démarrage du jeu, après l'ajout et la suppression de nombreux composants et entités. En général, lorsque le jeu démarre, vous pouvez ajouter toutes les entités et les composants pertinents ensemble, auquel cas ils peuvent avoir un modèle d'accès séquentiel très ordonné avec une bonne localité spatiale. Après beaucoup de suppressions et d'insertions, vous pourriez finir par obtenir quelque chose comme le désordre ci-dessus.
Un moyen très simple d'améliorer cette situation consiste à trier simplement vos composants en fonction de l'ID / index d'entité qui les possède. À ce stade, vous obtenez quelque chose comme ceci:
Et c'est un modèle d'accès beaucoup plus convivial pour le cache. Ce n'est pas parfait car nous pouvons voir que nous devons sauter certains composants de rendu et de mouvement ici et là, car notre système ne s'intéresse qu'aux entités qui ont à la fois , et certaines entités n'ont qu'un composant de mouvement et certaines n'ont qu'un composant de rendu , mais au moins vous finissez par être en mesure de traiter certains composants contigus (plus en pratique, généralement, car souvent vous attacherez des composants intéressants, comme peut-être plus d'entités dans votre système qui ont un composant de mouvement auront un composant de rendu que ne pas).
Plus important encore, une fois que vous les avez triés, vous ne chargerez pas les données d'une région de mémoire dans une ligne de cache pour les recharger ensuite en une seule boucle.
Et cela ne nécessite pas une conception extrêmement complexe, juste une passe de tri radix en temps linéaire de temps en temps, peut-être après avoir inséré et supprimé un tas de composants pour un type de composant particulier, auquel cas vous pouvez le marquer comme besoin d'être trié. Un tri Radix raisonnablement implémenté (vous pouvez même le paralléliser, ce que je fais) peut trier un million d'éléments en environ 6 ms sur mon i7 quadricœur, comme illustré ici:
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 ]
Ce qui précède consiste à trier un million d'éléments 32 fois (y compris le délai memcpy
avant et après le tri des résultats). Et je suppose que la plupart du temps, vous n'aurez pas réellement plus d'un million de composants à trier, donc vous devriez très facilement être en mesure de vous faufiler de temps en temps sans provoquer de bégaiements notables de la fréquence d'images.