2
\$\begingroup\$

I have been trying to wrap my head around how ECS works when there are components which are shared or dependent. I've read numerous articles on ECS and can't seem to find a definitive answer to this.

Assume the following scenario:

I have an entity which has a ModelComponent (or MeshComponent), a PositionComponent and a ParticlesComponent (or EmitterComponent).

The ModelRenderSystem needs both the ModelComponent and the PositionComponent.

The ParticleRenderSystem needs ParticlesComponent and the PositionComponent.

In the ModelRenderSystem, for cache efficiency / locality, I would like run through all the ModelComponents which are in a compact array and render them, however for each model I need to pull the PositionComponent. I haven't even started thinking about how to deal with the textures, shaders etc for each model (which will definitely blow the cache).

A similar issue with the ParticleRenderSystem.. I need both the ParticlesComponent as well as the PositionComponent, and I want to be able to run through all ParticlesComponents in a cache efficient / friendly manner.

I considered having ModelComponent and ParticlesComponent each having their own position, but they will need to be synched every time the models position changes (imagine a particle effect on a character). This adds another entity or component which needs to track and synch components or values (and potentially negates any cache efficiency).

How does everyone else handle these kinds of dependency issues?

\$\endgroup\$
  • 2
    \$\begingroup\$ Very very related: Why is it so bad to optimize too early? (optimizing for cache misses is most definitely a premature optimization) \$\endgroup\$ – Charanor Nov 14 '17 at 15:59
  • \$\begingroup\$ @Charanor given that optimizing for the cache can trigger a complete rewrite if you designed wrong from the start, I wouldn't say so. \$\endgroup\$ – Quentin Nov 14 '17 at 16:39
  • \$\begingroup\$ As @Quentin said, trying to make a big change later can sometimes be a massive effort. I am trying to build at least the bare minimum optimizations into the core framework. \$\endgroup\$ – Driv Nov 15 '17 at 2:59
  • 1
    \$\begingroup\$ I think this is valid question. Premature optimatization is bad, but having main architecture right from the start, is a good thing \$\endgroup\$ – Katu Nov 15 '17 at 12:26
4
\$\begingroup\$

Reducing cache misses doesn't need to mean getting rid of them entirely, so we do need to be wary of "making the perfect the enemy of the good enough"

After all, the absolute worst performance your game can have is a game that never runs at all because you're still working out every detail of the best possible ECS.

If you store all your mesh rendering components contiguously in one array, and all your transforms contiguously in another, then you can expect the bare minimum of cache misses from the former when iterating over it in order. Awesome!

Now, you could still get cache misses when looking up the corresponding transform — say, due to the ordering or your entities, you have a cluster of transforms corresponding to rendered meshes, then a cluster corresponding to physics triggers with no mesh, then back to mesh-havers again. You'll probably take a cache miss hopping over that gap, but that's still a fair shake better than a cache miss for every component strewn randomly through memory. It doesn't have to be perfect to be worthwhile.

Also, since your transforms are likely small and frequently used by many systems, you have a decent chance that some of the ones you need are still hanging out in one of your cache levels from a previous step, even if your lookups weren't perfectly contiguous & pre-fetchable on this pass.

So, try the simplest version that makes sense to you, and profile it on some imitation data mirroring what you expect common use cases to look like. See if you still have more work to do to shave down the cache misses even further, or if other parts of your code are actually a bigger problem in practice (or maybe it's all performing up to your targets already and you're done!)

See also this recent thread about approaches to sorting components to minimize gaps when iterating

\$\endgroup\$
  • \$\begingroup\$ thanks.. some great advice here... Will give this some thought. \$\endgroup\$ – Driv Nov 15 '17 at 3:02
2
\$\begingroup\$

Cache coherency is important, but it's not the be-all, end-all of everything.

It is in the general case impossible to arrange for your code to provide maximally-coherency access to all the necessary data for everything that needs some data. You can do it for small, simple projects, but as things get complex you start having to do tricks like duplicating data or compressing it, and that starts to cancel out some of your coherency gains.

You're generally only going to be able to make a subset of your update processes "perfectly" cache-coherent. You should profile to determine which ones are the most likely to benefit from doing so, based on your game's actual access patterns and distribution of objects, and arrange the data accordingly. Everything else will have to settle for being less-than-ideally cache-coherent, or simply not that coherent at all.

Optimizing for cache coherency is usually more about focusing on the data access patterns, which is often at odds with dogmatic adherence to classification (with both "traditional OO" and "ECS" approaches).

Since position is generally small, it's a reasonable candidate for (effective) duplication: you could consider an approach where the models and particle systems have their own local position, which is an offset from the real position. You can process the arrays of models and arrays of particle systems in that local space without having to pull in a shared position from elsewhere until a later stage.

But at a higher level I'd consider simply not worrying about the cache impact too much here. Yet. Both of the systems you're worried about are render-related, so "all of them in one big array" is perhaps not the best storage mechanism anyway. You're going to want to do a lot of culling and other kinds of visibility rejection on a lot of these, and that results of that rejection may shuffle the interesting set of of objects around quite a bit, unrelated to their original positions in the array.

Similarly you're going to want to group them around efficient use of actual rendering resources (avoiding state switches, sharing resources where possible, et cetera), which also may imply a different data structure than an array. And you note that you're not even sure what you're doing about "textures and shaders" for each model, which suggests you're not feature-complete there.

Certainly there is cause for concern about having to do make refactoring after the fact if you really muck up the design of a system. But I would not let that prevent you from first making the system work, with all the features you want. Once it works and does everything you want, then you can make it fast, because you'll have a better idea of what you requirements and constraints actually are.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the advice. This is actually my second pass on the core framework. I am not traditionally a game programmer but have been building software for a long time. My first pass was a traditional OO framework which was working great, but was a bit rigid. Looking around led me to ECS which seems really nice, so I started rewriting parts of the framework to support ECS. I have the basic ECS framework in place and working, at which point I then came across articles on cache locality. There always seems to be some better way of doing things with game development :-) \$\endgroup\$ – Driv Nov 15 '17 at 3:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.