Installer
J'ai une architecture de composant d'entité où les entités peuvent avoir un ensemble d'attributs (qui sont des données pures sans comportement) et il existe des systèmes qui exécutent la logique d'entité qui agissent sur ces données. Essentiellement, dans un peu de pseudo-code:
Entity
{
id;
map<id_type, Attribute> attributes;
}
System
{
update();
vector<Entity> entities;
}
Un système qui se déplace le long de toutes les entités à un rythme constant pourrait être
MovementSystem extends System
{
update()
{
for each entity in entities
position = entity.attributes["position"];
position += vec3(1,1,1);
}
}
Essentiellement, j'essaie de paralléliser update () aussi efficacement que possible. Cela peut être fait en exécutant des systèmes entiers en parallèle ou en donnant à chaque mise à jour () d'un système deux composants afin que différents threads puissent exécuter la mise à jour du même système, mais pour un sous-ensemble différent d'entités enregistrées avec ce système.
Problème
Dans le cas du MovementSystem montré, la parallélisation est triviale. Étant donné que les entités ne dépendent pas les unes des autres et ne modifient pas les données partagées, nous pourrions simplement déplacer toutes les entités en parallèle.
Cependant, ces systèmes nécessitent parfois que les entités interagissent les unes avec les autres (lecture / écriture de données de / vers), parfois au sein du même système, mais souvent entre différents systèmes qui dépendent les uns des autres.
Par exemple, dans un système de physique, les entités peuvent parfois interagir les unes avec les autres. Deux objets entrent en collision, leurs positions, vitesses et autres attributs sont lus à partir d'eux, sont mis à jour, puis les attributs mis à jour sont réécrits dans les deux entités.
Et avant que le système de rendu dans le moteur puisse commencer à rendre des entités, il doit attendre que les autres systèmes terminent l'exécution pour s'assurer que tous les attributs pertinents sont ce dont ils ont besoin.
Si nous essayons de paralléliser aveuglément cela, cela conduira à des conditions de course classiques où différents systèmes peuvent lire et modifier des données en même temps.
Idéalement, il existerait une solution où tous les systèmes pourraient lire les données de toutes les entités qu'ils souhaitent, sans avoir à se soucier que d'autres systèmes modifient ces mêmes données en même temps, et sans que le programmeur se soucie de l'ordre correct de l'exécution et de la parallélisation de ces systèmes manuellement (ce qui n'est parfois même pas possible).
Dans une implémentation de base, cela pourrait être réalisé en plaçant simplement toutes les lectures et écritures de données dans des sections critiques (en les gardant avec des mutex). Mais cela induit une grande quantité de temps d'exécution et n'est probablement pas adapté aux applications sensibles aux performances.
Solution?
À mon avis, une solution possible serait un système où la lecture / la mise à jour et l'écriture des données sont séparées, de sorte que dans une phase coûteuse, les systèmes ne lisent que les données et calculent ce dont ils ont besoin pour calculer, mettent en cache les résultats, puis écrivent tous les données modifiées reviennent aux entités cibles dans une passe d'écriture distincte. Tous les systèmes agiraient sur les données dans l'état où elles se trouvaient au début de la trame, puis avant la fin de la trame, lorsque tous les systèmes ont terminé la mise à jour, une passe d'écriture sérialisée se produit où la mise en cache résulte de tous les différents les systèmes sont itérés et réécrits aux entités cibles.
Ceci est basé sur l'idée (peut-être fausse?) Que la victoire de la parallélisation facile pourrait être suffisamment importante pour surpasser le coût (à la fois en termes de performances d'exécution et de surcharge de code) de la mise en cache des résultats et de la passe d'écriture.
La question
Comment un tel système pourrait-il être mis en œuvre pour obtenir des performances optimales? Quels sont les détails d'implémentation d'un tel système et quelles sont les conditions préalables pour un système Entity-Component qui souhaite utiliser cette solution?
la source
J'ai entendu parler d'une solution intéressante à ce problème: L'idée est qu'il y aurait 2 copies des données d'entité (gaspillage, je sais). Une copie serait la copie actuelle et l'autre serait la copie précédente. La copie actuelle est strictement écrite et la copie précédente est strictement en lecture seule. Je suppose que les systèmes ne veulent pas écrire dans les mêmes éléments de données, mais si ce n'est pas le cas, ces systèmes devraient être sur le même thread. Chaque thread aurait un accès en écriture aux copies actuelles des sections mutuellement exclusives des données, et chaque thread a un accès en lecture à toutes les copies précédentes des données, et peut donc mettre à jour les copies actuelles en utilisant les données des copies précédentes sans aucune verrouillage. Entre chaque image, la copie actuelle devient la copie précédente, mais vous souhaitez gérer l'échange de rôles.
Cette méthode supprime également les conditions de concurrence, car tous les systèmes fonctionneront avec un état périmé qui ne changera pas avant / après que le système l'ait traité.
la source
Je connais 3 conceptions logicielles gérant le traitement parallèle des données:
Voici quelques exemples pour chaque approche pouvant être utilisée dans un système d'entités:
CollisionSystem
qui litPosition
etRigidBody
composants et devrait mettre à jour unVelocity
. Au lieu de manipuler leVelocity
directement, leCollisionSystem
testament place plutôt unCollisionEvent
dans la file d'attente de travail d'unEventSystem
. Cet événement sera ensuite traité séquentiellement avec d'autres mises à jour duVelocity
.EntitySystem
définit un ensemble de composants dont il a besoin pour lire et écrire. Pour chacun,Entity
il acquiert un verrou de lecture pour chaque composant qu'il souhaite lire, et un verrou d'écriture pour chaque composant qu'il souhaite mettre à jour. Comme cela, toutEntitySystem
le monde pourra lire simultanément les composants pendant que les opérations de mise à jour sont synchronisées.MovementSystem
, lePosition
composant est immuable et contient un numéro de révision . LeMovementSystem
lit savely lePosition
etVelocity
composants et calcule la nouvellePosition
, incrémenter la lecture révision numéro et tente mise à jour duPosition
composant. En cas de modification simultanée, le framework l'indique sur la mise à jour etEntity
sera remis dans la liste des entités qui doivent être mises à jour par leMovementSystem
.Selon les systèmes, les entités et les intervalles de mise à jour, chaque approche peut être bonne ou mauvaise. Une structure de système d'entité peut permettre à l'utilisateur de choisir entre ces options pour modifier les performances.
J'espère que je pourrais ajouter quelques idées à la discussion et s'il vous plaît faites le moi savoir s'il y a des nouvelles à ce sujet.
la source