Preparar
Tengo una arquitectura de entidad-componente donde las entidades pueden tener un conjunto de atributos (que son datos puros sin comportamiento) y existen sistemas que ejecutan la lógica de la entidad que actúa sobre esos datos. Esencialmente, en algo pseudocódigo:
Entity
{
id;
map<id_type, Attribute> attributes;
}
System
{
update();
vector<Entity> entities;
}
Un sistema que simplemente se mueve a lo largo de todas las entidades a una velocidad constante podría ser
MovementSystem extends System
{
update()
{
for each entity in entities
position = entity.attributes["position"];
position += vec3(1,1,1);
}
}
Esencialmente, estoy tratando de paralelizar update () de la manera más eficiente posible. Esto se puede hacer ejecutando sistemas completos en paralelo, o dando a cada actualización () de un sistema un par de componentes para que diferentes subprocesos puedan ejecutar la actualización del mismo sistema, pero para un subconjunto diferente de entidades registradas con ese sistema.
Problema
En el caso del Movimiento mostrado, la paralelización es trivial. Como las entidades no dependen unas de otras y no modifican los datos compartidos, podríamos mover todas las entidades en paralelo.
Sin embargo, estos sistemas a veces requieren que las entidades interactúen (lean / escriban datos de / a) entre sí, a veces dentro del mismo sistema, pero a menudo entre diferentes sistemas que dependen unos de otros.
Por ejemplo, en un sistema de física a veces las entidades pueden interactuar entre sí. Dos objetos colisionan, sus posiciones, velocidades y otros atributos se leen, se actualizan y luego los atributos actualizados se vuelven a escribir en ambas entidades.
Y antes de que el sistema de representación en el motor pueda comenzar a representar entidades, debe esperar a que otros sistemas completen la ejecución para garantizar que todos los atributos relevantes sean lo que necesitan ser.
Si intentamos paralelizar ciegamente esto, dará lugar a condiciones de carrera clásicas donde diferentes sistemas pueden leer y modificar datos al mismo tiempo.
Idealmente, existiría una solución en la que todos los sistemas puedan leer datos de las entidades que deseen, sin tener que preocuparse de que otros sistemas modifiquen esos mismos datos al mismo tiempo, y sin que el programador se preocupe por ordenar correctamente la ejecución y la paralelización de estos sistemas de forma manual (que a veces puede que ni siquiera sea posible).
En una implementación básica, esto podría lograrse simplemente colocando todas las lecturas y escrituras de datos en secciones críticas (protegiéndolas con mutexes). Pero esto induce una gran cantidad de sobrecarga en tiempo de ejecución y probablemente no sea adecuado para aplicaciones sensibles al rendimiento.
¿Solución?
En mi opinión, una posible solución sería un sistema en el que la lectura / actualización y la escritura de datos estén separadas, de modo que en una fase costosa, los sistemas solo lean datos y calculen lo que necesitan para computar, de alguna manera guarden en caché los resultados y luego escriban todos los datos modificados vuelven a las entidades de destino en un pase de escritura separado. Todos los sistemas actuarían sobre los datos en el estado en que se encontraban al principio del marco, y luego antes del final del marco, cuando todos los sistemas hayan terminado de actualizarse, se produce un pase de escritura serializado donde el caché resulta de todos los diferentes Los sistemas se repiten y se vuelven a escribir en las entidades de destino.
Esto se basa en la idea (¿quizás errónea?) De que la ganancia de paralelización fácil podría ser lo suficientemente grande como para superar el costo (tanto en términos de rendimiento de tiempo de ejecución como de sobrecarga de código) del almacenamiento en caché de resultados y el pase de escritura.
La pregunta
¿Cómo podría implementarse un sistema de este tipo para lograr un rendimiento óptimo? ¿Cuáles son los detalles de implementación de dicho sistema y cuáles son los requisitos previos para un sistema de entidad-componente que quiere usar esta solución?
fuente
He oído hablar de una solución interesante a este problema: la idea es que habría 2 copias de los datos de la entidad (desperdicio, lo sé). Una copia sería la copia actual, y la otra sería la copia pasada. La copia actual es estrictamente de solo escritura, y la copia pasada es estrictamente de solo lectura. Supongo que los sistemas no quieren escribir en los mismos elementos de datos, pero si ese no es el caso, esos sistemas deberían estar en el mismo hilo. Cada subproceso tendría acceso de escritura a las copias actuales de secciones mutuamente excluyentes de los datos, y cada subproceso tiene acceso de lectura a todas las copias anteriores de los datos y, por lo tanto, puede actualizar las copias actuales utilizando datos de las copias anteriores sin cierre. Entre cada fotograma, la copia actual se convierte en la copia pasada, sin embargo, desea manejar el intercambio de roles.
Este método también elimina las condiciones de carrera porque todos los sistemas funcionarán con un estado obsoleto que no cambiará antes / después de que el sistema lo haya procesado.
fuente
Conozco 3 diseños de software que manejan el procesamiento paralelo de datos:
Aquí hay algunos ejemplos para cada enfoque que pueden usarse en un sistema de entidad:
CollisionSystem
que leePosition
yRigidBody
componentes y debería actualizar aVelocity
. En lugar de manipularVelocity
directamente, laCollisionSystem
voluntad colocará unCollisionEvent
en la cola de trabajo de unEventSystem
. Este evento se procesará secuencialmente con otras actualizaciones delVelocity
.EntitySystem
define un conjunto de componentes que necesita leer y escribir. Para cada unoEntity
, obtendrá un bloqueo de lectura para cada componente que desee leer y un bloqueo de escritura para cada componente que desee actualizar. De esta manera, todosEntitySystem
podrán leer componentes simultáneamente mientras las operaciones de actualización están sincronizadas.MovementSystem
, elPosition
componente es inmutable y contiene un número de revisión . ElMovementSystem
lee la SavelyPosition
yVelocity
componentes y calcula la nuevaPosition
, incrementando la lectura de revisión número e intenta actualizar elPosition
componente. En caso de una modificación concurrente, el marco indica esto en la actualización y seEntity
volverá a colocar en la lista de entidades que elMovementSystem
.Dependiendo de los sistemas, entidades e intervalos de actualización, cada enfoque puede ser bueno o malo. Un marco de sistema de entidad podría permitir al usuario elegir entre esas opciones para ajustar el rendimiento.
Espero poder agregar algunas ideas a la discusión y por favor avíseme si hay alguna noticia al respecto.
fuente