State as Structural Change vs. State In Data

Architecture feedback wanted - I more and more find myself applying a pattern that encodes structural state in the actual ComponentData itself, e.g.

struct Aim : IComponentData
{
  public bool enabled;   //this, rather than the presence/absence of the Aim component on an entity!

  public float3 position; //Data is interpreted / ignored depending on state
  public Entity target;   //I also  tried to interpret the data as state directly, but it reads ambiguously and masks broken contracts ("target cannot be Entity.Null")
}

And then I have systems ignore the entities where enabled is false. (i.e. if (!aim.enabled) return; )

In this case, because whole kinematic hierarchies of objects aim based on what the main actor sees, I can also just copy this component down to the dependents instead of changing the structure on 3 entities x number_of_guns_on_a_ship every time I run out of targets.

While I believe this saves the burden and memcpy that structural changes can bring with them, and keeps entity state available earlier (not at the sync point of whatever structural change ECBS you’re recruiting to do the work), it does cause the system to run for a lot more entities.

In other cases, it saves me a second system that works on entities that don’t have the component, but need to add it, etc. But it means my primary system now does twice the work. I find that if the work is simple enough (e.g. < 2x the amount of code the considerable boilerplate and dependency headache of a System would be), it is well worth the trade-off.

A last advantage, it also makes the system much more parallelization friendly (also by merit of having fewer systems accessing the same data).

What are your thoughts? Is “state in structure” better or worse than “state in data”? What are the cases where you opt for one or the other?

How do you make structural changes require less boilerplate (get the right ECBS, get EntityCommandBuffer from ECBS, don’t forget to make it concurrent, add producer handle to ECBS…)

This kind of pattern match the description of how they implented thing in the FPS sample.
Unity is working on that enable/disable component state built in ECS itself. It would avoid strucutral change and allow query to filter out entity with disabled component so thath it does not run on them.

1 Like

The only way structural changes scale well big picture is where you can wait until the next frame for changes to resolve. If you design your game around liberal usage of add/remove component you will end up with entangled dependencies that will force sync points before jobs are finished.

So what you are doing is preferable in many cases. Structural changes are fine for cases where the data is isolated well enough to not reach out and touch other stuff where it can cause havoc. It’s just difficult to do that in a real game as designs are constantly changing and what wasn’t dependent before now is.

The mistake that a lot make here is thinking it’s structural change performance that is an issue. In a significant project it’s just not, the hard problem is ensuring structural changes aren’t force completing work that’s not finished yet.

2 Likes

IMO, like the answer to most interesting questions, it depends (and/or “test it if you’re very concerned”). I can see cases where one or the other would be faster, but it depends so much on the number of entities, how many are enabled, how complex the calculations are, the size of those entities (loading a 32 byte entity is a lot faster than a 1kb entity), and the actual hardware.

With the if(enabled) solution you have (and enough entities for it to matter), the performance is going to depend on CPU instruction pipelining, in particular branch prediction. When almost all the entities are enabled, the branch prediction works, and there’s little slowdown. When almost all are disabled, there’s no branches, but you’re accessing much more memory than you need to (and that’s slower than mispredicted branches). With a mix of enabled and disabled, you’re going to be have pipeline stalls.

How bad is a mispredicted branch? Depending on the hardware, you’re often looking at 10-30 cycles*. This is a problem when there are very many entities and the inner calculation is simple. However, if that calculation is simple enough, it can be turned into a conditional move or other arithmetic, and the branch is eliminated entirely. If you have a complex aiming calculation, those 10-30 cycles stop mattering. But there’s some “bitter spot” where the calculations require a branch, but are just simple enough for the pipeline stalls to matter (eg: java - Why is processing a sorted array faster than processing an unsorted array? - Stack Overflow )

tl;dr: Just do whatever is simplest to code, fix it later if it becomes a problem.

  • Sources on cost of branch misprediciton are surprisingly hard to find. The 10-30 number is something I remember reading in Agner Fog [ https://www.agner.org/optimize/microarchitecture.pdf ] book a while back, but for the life of me I can’t find it. If you’re at all interested in these super low-level things, all the Agner books are free online and 100% worth reading. But then you’re going to be reading a lot of mostly-irrelevant stuff instead of developing your game.

Some more modern sources seem to confirm it in the 10-30 cycle range, eg:

https://lemire.me/blog/2019/10/15/mispredicted-branches-can-multiply-your-running-times/

You’ll also have poorer cache utilization, since you’ll be loading unused data into cache. Some of that net effect is captured in those tests, but this kind of stuff is really hard to guess/reason about because so many tiny things can affect it. Profiling/testing on the target hardware is the only way to be sure.

3 Likes