Configuração
Eu tenho uma arquitetura de componente de entidade em que as entidades podem ter um conjunto de atributos (que são dados puros sem comportamento) e existem sistemas que executam a lógica da entidade que atua sobre esses dados. Essencialmente, em algum pseudocódigo:
Entity
{
id;
map<id_type, Attribute> attributes;
}
System
{
update();
vector<Entity> entities;
}
Um sistema que apenas se move ao longo de todas as entidades a uma taxa constante pode ser
MovementSystem extends System
{
update()
{
for each entity in entities
position = entity.attributes["position"];
position += vec3(1,1,1);
}
}
Essencialmente, estou tentando paralelizar update () da maneira mais eficiente possível. Isso pode ser feito executando sistemas inteiros em paralelo ou fornecendo a cada atualização () de um sistema alguns componentes para que threads diferentes possam executar a atualização do mesmo sistema, mas para um subconjunto diferente de entidades registradas nesse sistema.
Problema
No caso do MovementSystem mostrado, a paralelização é trivial. Como as entidades não dependem umas das outras e não modificam os dados compartilhados, poderíamos simplesmente mover todas as entidades em paralelo.
No entanto, esses sistemas às vezes exigem que as entidades interajam (leiam / gravem dados de / para) umas às outras, às vezes dentro do mesmo sistema, mas geralmente entre sistemas diferentes que dependem uns dos outros.
Por exemplo, em um sistema de física, algumas vezes as entidades podem interagir umas com as outras. Dois objetos colidem, suas posições, velocidades e outros atributos são lidos a partir deles, são atualizados e, em seguida, os atributos atualizados são gravados novamente nas duas entidades.
E antes que o sistema de renderização no mecanismo possa iniciar a renderização de entidades, é necessário aguardar que outros sistemas concluam a execução para garantir que todos os atributos relevantes sejam o que precisam ser.
Se tentarmos paralelizar cegamente isso, isso levará a condições clássicas de corrida, nas quais diferentes sistemas podem ler e modificar dados ao mesmo tempo.
Idealmente, existiria uma solução em que todos os sistemas possam ler dados de qualquer entidade que desejarem, sem ter que se preocupar com outros sistemas modificando esses mesmos dados ao mesmo tempo e sem que o programador se preocupe em ordenar adequadamente a execução e paralelização de esses sistemas manualmente (o que às vezes nem é possível).
Em uma implementação básica, isso pode ser conseguido colocando todas as leituras e gravações de dados em seções críticas (protegendo-as com mutexes). Mas isso induz uma grande quantidade de sobrecarga de tempo de execução e provavelmente não é adequado para aplicativos sensíveis ao desempenho.
Solução?
Em minha opinião, uma solução possível seria um sistema onde a leitura / atualização e gravação de dados são separadas, de modo que, em uma fase cara, os sistemas apenas leiam dados e calculem o que precisam calcular, de alguma forma, armazenem em cache os resultados e depois escrevam todos os dados alterados de volta para as entidades de destino em um passe de gravação separado. Todos os sistemas atuariam com os dados no estado em que estavam no início do quadro e, em seguida, antes do final do quadro, quando todos os sistemas terminassem de atualizar, uma passagem de gravação serializada acontecerá onde o cache resulta de todos os diferentes os sistemas são iterados e gravados de volta para as entidades de destino.
Isso se baseia na (talvez errada?) Idéia de que a vitória fácil da paralelização poderia ser grande o suficiente para superar o custo (tanto em termos de desempenho de tempo de execução quanto de sobrecarga de código) do cache de resultados e da passagem de gravação.
A questão
Como esse sistema pode ser implementado para alcançar o desempenho ideal? Quais são os detalhes de implementação desse sistema e quais são os pré-requisitos para um sistema Entity-Component que deseja usar esta solução?
fonte
Ouvi falar de uma solução interessante para esse problema: a idéia é que haveria 2 cópias dos dados da entidade (desperdício, eu sei). Uma cópia seria a cópia atual e a outra seria a cópia anterior. A cópia atual é estritamente somente para gravação e a cópia anterior é estritamente somente para leitura. Estou assumindo que os sistemas não desejam gravar nos mesmos elementos de dados, mas se esse não for o caso, esses sistemas deverão estar no mesmo encadeamento. Cada encadeamento teria acesso de gravação às cópias presentes de seções mutuamente exclusivas dos dados, e cada encadeamento terá acesso de leitura a todas as cópias passadas dos dados e, portanto, poderá atualizar as cópias presentes usando os dados das cópias anteriores sem bloqueio. Entre cada quadro, a cópia atual se torna a cópia anterior, no entanto, você deseja lidar com a troca de funções.
Esse método também remove as condições de corrida porque todos os sistemas estarão trabalhando com um estado obsoleto que não será alterado antes / depois que o sistema o processar.
fonte
Conheço três projetos de software que lidam com processamento paralelo de dados:
Aqui estão alguns exemplos para cada abordagem que pode ser usada em um sistema de entidades:
CollisionSystem
que lêPosition
eRigidBody
componentes e deve atualizar aVelocity
. Em vez de manipularVelocity
diretamente, oCollisionSystem
will colocará umCollisionEvent
na fila de trabalho de umEventSystem
. Esse evento será processado sequencialmente com outras atualizações noVelocity
.EntitySystem
define um conjunto de componentes que ele precisa ler e gravar. Para cadaEntity
um deles , será gerado um bloqueio de leitura para cada componente que deseja ler e um bloqueio de gravação para cada componente que deseja atualizar. Assim, todosEntitySystem
poderão ler componentes simultaneamente enquanto as operações de atualização são sincronizadas.MovementSystem
, oPosition
componente é imutável e contém um número de revisão . AMovementSystem
lê savely oPosition
eVelocity
componentes e calcula o novoPosition
, incrementando a leitura revisão número e tenta atualizar oPosition
componente. No caso de uma modificação simultânea, a estrutura indica isso na atualização eEntity
será recolocada na lista de entidades que precisam ser atualizadas peloMovementSystem
.Dependendo dos sistemas, entidades e intervalos de atualização, cada abordagem pode ser boa ou ruim. Uma estrutura de sistema de entidades pode permitir que o usuário escolha entre essas opções para ajustar o desempenho.
Espero poder adicionar algumas idéias à discussão e por favor me avise se houver alguma notícia sobre isso.
fonte