ECS Model - Updating output components efficiently

Started by
6 comments, last by All8Up 4 years, 7 months ago

Hello there,

I'm currently implementing an ECS model and wanted to get some feedback from the community regarding a concern.

In my ECS model, entities are simply IDs that associate components. Each component possesses an "entity ID" member such that systems can determine who each component belongs to. My components live in "component pools" that are grabbed and released as needed by an "EntityManager". The component pool ensures that all active components are living in contiguous memory. For example, let's say I have a component pool of 20 active components. If I were to release component 5, I would essentially move component 20 to the vacant slot at position 5 and then decrement the active count to 19. This ensure there are no empty spots when my systems iterate over component data.

This implementation seemed to make sense to me. However, I'm now running into the realization that my systems will likely be operating on multiple component types. For example, let's say I have a "Health System" that reads in a "collision component" and updates a "health component". My model is set up such that I can efficiently iterate through components within a single pool. But because components are organized within their own pools, there is no guarantee that components belonging to the same entity share the same position in their respective component pools. For example, component 1 of the collision component pool might belong to entity 63, but component 1 of the health component pool might belong to entity 5. 

Essentially, my main concern is that my ECS model will be efficient in reading input components, but it will be inefficient in writing output components. I am not concerned with the book-keeping (each component pool contains an "entity map" such that I can easily grab a component by providing its entity ID). I am simply concerned about the efficiency of updating the output components.

Is this a valid concern? 

Edit: I would like to expand on my thoughts. 

When I have systems reading input components, I would consider this efficient because the system is iterating upward through contiguous memory (there is no jumping around to different addresses). When the system is updating output components however, I would consider this less efficient because the system has to write to memory that is not simply incrementing addresses (e.g. update component 20, update component 2, update component 25, etc.) However, I've started to realize that even though I'm "jumping around", the memory is still relatively close to one another. Would I need to be concerned about cache misses in this case? For some reason I've always assumed memory caching grabbed increasing address memory only. 

Advertisement

Thomas, your concern is valid, and the fact that you have this concern shows that you are thinking about memory access patterns, which is good. It may not be (probably is not) possible to achieve a single ideal memory layout for multiple scenarios (e.g. iterating/searching vs. O(1) lookup + writing), so choose the memory layout that is the best compromise and maximizes performance for the most common case. The typical "ECS" solution is criticized by many developers as being a lazy approach that does not solve specific problems, but instead tries to apply a one-size-fits-all solution to many different problems. With ECS you should not focus on its purported performance advantages, you should instead consider whether you need to be able to dynamically add and remove components to your game objects at runtime - THAT is what that system was designed for. The contiguous memory/performance advantage thing is not grounded in actually engineering a solution, it's more like a "happy side effect" that some developers will call good enough. If you don't want to think too deeply about individual memory layout and access decisions as you build systems, then you could do a lot worse than simply slapping ECS over the whole thing, and just try to balance the size of components and their granularity as best you can.

I think this is a valid observation. All the code you see on the internet is usually a gross over-simplification of how things work in practice. There is a great presentation by Blizzard about their implementation of an ECS in Overwatch. They show a visualization and systems usually access a couple of components. The transform component is usually something that is pulled in by a lot of system. Here is a link to the video in case you haven't seen it:

One way to think about this is to distinguish between primary and secondary components. Each system has exactly one primary component where the heavy lifting work is happening. So it is good to have this is a cache friendly access pattern. You still need to push and pull other state and you want to keep this to a minimum

For some systems it might also make sense to duplicate some state. E.g. physics engines usually caches the transform state of an enity (e.g the rigid body transform). How this is usually implemented in an engine is that the game keeps a dirty list of entities with updated transform components. Whenever you query the physics system (e.g. raycast or stepping the simulation) the dirty entity transform is broadcasted to the physic system. Here is a blog post how this is done in Unity:

https://blogs.unity3d.com/2017/06/29/best-practices-from-the-spotlight-team-optimizing-the-hierarchy/

So there is no general solution to your problem. At least I am not aware of one. I think your solution should depend on the problems (e.g. feature requirements) you need to solve for your game and personally I would favor things that are simple and can be communicated easily when working in large teams. 

 

PS: I agree with y2kiah posts as well. For me ECS is not so much about performance. All this DOTS stuff is a lot of Fugazi in my opinion. Personally I like how artists can compose game assets and gameplay in an ECS (e.g. it just is a nice workflow). It is much more artist centric as opposed to programmer centric workflow as in the earlier days of game development. And I think this is a good thing! You can also enforce physical decoupling of systems with an ECS. I think Blizzard requires systems to be implemented in CPP files. This way you prevent dependencies between systems. Personally I am much more concerned about complexity these days with game engine code than performance. Performance problems are usually easy to resolve for experienced engineers. Untangling messy spaghetti code is much more work and tedious.

 

This was really good feedback, I appreciate it you guys.

What I'm discovering is that I really don't know how I should organize my data prior to designing the systems. For example, it seems reasonable that an entity could have a "shape component" and "position component". However, when my render system wants to render and object, it now has to iterate over two component pools in order to draw an entity. In reality, the system will iterate over one component pool, and then have to search another component pool for the corresponding component.

This problem could easily be solved if I simply created a combined "position-shape component" that contains both position and shape data. With this component, a render system would only have to iterate over a single pool. 

Overall, I'm struggling with philosophy on how to organize my data. 

Edit: I will say that one method for resolving how data should be organized is to ask the question "would data in component A ever be useful without data in component B?". In my previous example of position vs shape component, that question would become "Would I ever need to know the shape of something without needing to know its position?". My answer would be "No, I would typically need both pieces of information". Thus the data could be combined into a single component. 

Hey Thomas,

I'd like to share some of my own architecture design ideas about how I implement "ECS" in my engine. For me I first followed the definition of ECS without too much alteration, thus the Entity is ID, the Component is POD of some logically coherent data and the System is the manager of the component instances. But later I realized and observed that this architecture implicates another important rule, that you must design the abstraction level of the system very clearly. That means you can't easily use your gameplay related components directly in your lower-level systems, because for the lower-level systems such like rendering or network system the data they needed actually is some deduced data from your higher-level components.

So if you organized your higher-level data to some human-friendly components, then you may need to add additional intermediate components for lower-level systems to access in a more efficient way; or, you may need to separate or aggregate several of your higher-level components to more or fewer components until you could reduce the redundant traversal over the entire component pool. My current system design has two different responsibility types, one is focusing on the single component type related business, such like LightComponent and LightComponentSystem, which only have the responsibility to manage the corresponding component's data change, and they have the producer role to produce intermediate data for other lower-level consumer systems to use; another one is focusing on tasks and policies like rendering, physics simulation, and OS event handling, they typically only have the consumer role.

Because my engine is a job-based parallel model currently so each system's time-domain dependency is naturally handled (You could reference to Naughty Dog or Budgie's talk and publication for more information). The whole data processing pipeline won't have too many duplicated operations due to this factor, because each upstream systems only iterate some components once and feed the result to the downstream systems when the time is suitable.

And in my personal point of view, nowadays I'd rather consider ECS as an architectural result of the Functional Programming paradigm and DOD, if you build your game and engine in a quite FP mindset then sooner or later you'll invented ECS by yourself, don't be limited by it and try to use the more general philosophy behind to solve your problems.

Hope it could help you a little. Happy coding?️?

On 8/26/2019 at 4:09 PM, Thomas Izzo said:

For example, component 1 of the collision component pool might belong to entity 63, but component 1 of the health component pool might belong to entity 5. 

I have never built an ECS before (that's the disclaimer ?) but I would probably aim to avoid that scenario altogether and keep components aligned so they can be indexed together.

You example was of a component being deleted, so perhaps that's just something to avoid: Don't remove components from entities. If components need to be 'dynamic' for an entity (in that it comes and goes) then perhaps just treat that like a state on the component and add a flag to activate/deactivate it

If you can't do that then you could give all components a pointer to a 'junction object' (which is "entity" if you like) that contains pointers to all the components for that entity, that way looking up a healthComponent from a collisionComponent could be done like this: 


collisionComp->entity->healthComp;

Where entity was a pointer to a struct that had a pointer to every possible component (most would be null perhaps):


struct Entity {
  CollisionComponent * collisionComp;
  HealthComponent * healthComp;
  // etc
}

This gives you constant-time access to related components and avoids scanning a pool of components for one with a matching entity ID (which is a linear-time operation)

In general, there are a number of issues glossed over in most presentations about ECS: how do you keep things efficient over the long run is an important item usually ignored.  Given that most ECS implementations focus on sparse arrays, this means that as components are added removed, your contiguous nature of things slows down as you are finding.  For my work this was unacceptable due to targeting 16+ core machines with millions of entities.

The largest question I ended up asking myself is "are sparse arrays worth it?"  Overall with millions of entities, nope, they were not worth it in most cases.  Now, let me clarify this, I'm not saying you don't *pack* things when you delete entities, what I'm saying is you don't maintain individual component sparsity.  Or another way of stating it is that each entity is associated with a single index value and that index value is used in each of the component arrays rather than going through a secondary index into a sparse array.  So, a single entity may have unused components in the arrays, in a typical use case for a large simulation like I was testing with, I was wasting a bit less than 1MB for ~1 million entities that took nearly 12MB's, oh the pain....  More importantly though, 99% of all entities were fully contiguous in memory both read and write components were also fully contiguous and my cache hit rates dropped by 80+% with 32 cores.

So, ask yourself, is 1Mb of memory worth removing 80+% of all cache misses caused by the eventual disorganized nature of sparse arrays?  Again, in my case: Hell yes it is worth it!  I did eventually end up regaining much of that wasted memory back but it was by starting with the synchronized flat array preference and only using sparse arrays in the very few cases where it made sense such as a camera component that was much of the 1Mb since it was fairly large and a waste for 99+% of all entities.

At this point you can keep expanding to solve further issues.  A problem I ended up having, and seems fairly common,  was that as the simulation expanded I ended up primarily with 4 typical groupings: ground unit, flying unit, projectile, decoration.  Basically they all shared the same components except one but that was the cause of most cache misses by that time.  This is where I ended up extending the system to use what some ECS systems call "groups" or "archetypes".  That's fairly detailed work at that point so I'll leave that to you to research or ask about specifically, generally speaking it is a fairly easy answer to the later problems which once again avoids needing per component sparsity.

Overall though, should you be concerned with cache misses?  Sure, if you plan on thousands or more entities.  Should you be using sparse arrays?  Depends, are you after the performance or the compact representation?  Can you get the best of both worlds?  To a degree but as with most things it becomes a case of diminishing returns and it is easy to go to far and end up with an over engineered solution which actually ends up detrimental to the actual goals.

This topic is closed to new replies.

Advertisement