5
\$\begingroup\$

Let's say I have an ECS with physics and I want to kill the player when a arrow hit him. So I have a callback when there is a collision. Now there is of course different behaviors with different collisions within 2 bodies (heal with heart, damage livings with weapons, etc...) so plenty of combinations of collisions.

I would think in ECS I will check the components each body has and call the appropriate behavior, but of course the more behaviors I have the more it will cost, because each combination should be checked for each collision.

More generally, don't ECS add more cost when there are more behaviors ? In traditional way (OOP with inheritance), no matter how many different behaviours you have (the count of derived classes), you will always pay only one method call. In ECS, you update each system at each frame, so the more systems you have the more it will cost, even if there is no entities that use it.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ From my experience, given a "small" simulation, the ECS did not help much with performance but rather with the ease of implementing new features. \$\endgroup\$
    – Vaillancourt
    Commented Mar 28, 2022 at 12:18
  • 2
    \$\begingroup\$ Think of it this way... If you have a thousand entities to move, do you want 1,000 method calls, with all the associated stack pushes and pops, local variable cleanup, etc OR do you want a single method that runs down a list of a thousand items and acts accordingly? ECS removes a LOT of the per-entity overhead. \$\endgroup\$
    – Basic
    Commented Mar 28, 2022 at 13:05
  • 1
    \$\begingroup\$ Performance boosts from data locality are unintuitive because they're due to how hardware behaves instead of the ideal behaviour we imagine. I think game programming patterns explains it well with a helpful analogy. \$\endgroup\$
    – idbrii
    Commented Mar 30, 2022 at 4:20
  • \$\begingroup\$ the optimisation is usualy done inside one frame, and than all others frame are otpimised. \$\endgroup\$
    – jon
    Commented Oct 26, 2022 at 19:28
  • \$\begingroup\$ We have some previous Q&A explaining how ECS can often deliver performance wins, which may be helpful to you. \$\endgroup\$
    – DMGregory
    Commented Sep 1 at 4:08

4 Answers 4

21
\$\begingroup\$

With an OOP architecture, you usually end up with code which if unrolled to a purely procedural style would look like this:

for each entity
   if entity can move
        move(entity)
   switch entity attacking type
       when shooter
         shoot(entity)
       when melee
         meleeAttack(entity)
       when nothing
         doNothing(entity)  
   if entity has health
        checkIfDead(entity)

While with an ECS architecture, the resulting logic would look more like this:

for each entity which can move
    move(entity)
for each entity which can shoot
    shoot(entity)
for each entity which can meleeAttack
    meleeAttack(entity)
for each entity which has health
    checkIfDead(entity)

Instead of one long loop over all entities, you get a series of short loops. Why is the second usually faster?

  1. Smaller loops are easier to optimize. This applies to both your compiler and to the CPU microcode.
  2. ECS allows those loops to only iterate over those objects which actually have those components. OOP needs to check at runtime. Polymorphism isn't magic. When you call a method of a base-class or interface, then it might look like a simple method call in your code. But what actually happens at runtime is a hidden switch statement to pick the correct implementing method based on the actual type. ECS, on the other hand, can pre-compute lists of which entities are relevant for each system and only needs to update those lists when the composition of an entity changes. So it no longer needs to check every tick.
  3. Memory locality. The move-loop only requires the move-components. The check-if-dead-loop only requires the health-components, and so on. In a well-designed ECS architecture, the data of all components of the same type will be stored in an array, which will be a contiguous section in memory. Contiguous memory section are very quick to load from RAM into CPU. In an OOP design, on the other hand, you are doing a loop over all the data of all the entities at the same time. Which means they all need to be loaded from RAM into the CPU cache. When you don't have enough CPU cache to hold them all, then this is going to get slow.
  4. Parallelizability. This is not something you get automatically. But if you want to design an engine which utilizes multiple CPU cores well, then ECS usually makes that easier than OOP. The main problem with parallelization is finding units of processing which don't share any data. The reasons for that would make this answer too long. So just trust me that you don't want two threads to access the same memory, especially not when at least one of them needs to change that data. The data-oriented design of ECS makes that much easier, because it makes it far more visible which systems use which data. That means it is usually easy to tell which systems can be run in parallel and which systems can use parallelization internally. But with OOP, every object is a black box (intentionally!). You don't know what data an object accesses, and you don't want to (it's called abstraction). That makes it very hard to judge what can be parallelized and what can not.
\$\endgroup\$
4
\$\begingroup\$

Usually, you don't have a single event to manage per frame, you need to manage a lost of asynchroneous events, periodic events, updates, etc., so the situation is more like "managing a variable but maybe huge number of things vs iterating on all the components".
ECS forces you to iterate over every component in your game/scene, but they are laid out in arrays, which favorizes fast-access on modern CPU architectures via the use of caches.
If you have very few events per frame, this can indeed by more costly to iterate over every component (but is this really an issue? cf below), but it might not be the case with more events.

As always, we can always formulate general guidelines, but the hard truth about software development is that it can be totally counter-intuitive due to the complexity of modern processors and the numerous efficient optimizations that compilers are capable of.
For this reason, in order to determine which way is the more efficient and where are your bottlenecks, you need to test and profile your application instead of relying on 'rules'.

When developing software, you don't try to have the best performance possible. Instead, you set up a target performance (which depends on the target hardware) and you try to achieve it. There is no need to spend 2 months trying to get your game loop running 2% faster if is already runs fast enough!

You also have to remember that performance is only one aspect to take into account with your code/architecture. It might be the most important constraint for a project, but this is usually the case only for embedded systems with very limited processing power. In general, it is part of a trade-off with other aspects: simplicity, maintainability, development speed, etc.
ECS can help with those because it is a very flexible architecture, which allows to make changes to entities easily.

Considering all of these, we can say that ECS is not per se a costly architecture performance-wise, and that performance depends both on the implementation and the specificities of your project.
Performance is only one criterion competing with others, and the best solution is always a trade-off between all of them that is specific to each project.

\$\endgroup\$
1
\$\begingroup\$

Although this question was asked a while ago, I find some of the assumptions worth discussing:

Let's say I have an ECS with physics and I want to kill the player when a arrow hit him. So I have a callback when there is a collision. Now there is of course different behaviors with different collisions within 2 bodies (heal with heart, damage livings with weapons, etc...) so plenty of combinations of collisions.

That's a list management problem. I don't see this as a problem with ECS, but with how a particular ECS is implemented. Instead of running full "list-of-everything against list-of-everything" potential collisions, you could set up certain specialised sub-lists that would divide your processing of things like "hearts hitting players to give them health" vs "arrows and other stuff that hurts hitting players to reduce their health". You could maintain those lists through deltas, rather than doing enormous list rebuilds every frame. You could also process some of these lists occasionally, not every frame.

These exhaustive if or switch statements you mention are, broadly speaking, a bad idea, given the cost that mis-predicted conditionals have on your CPU pipeline. I'd probably define a 32- or 64-bit set of binary flags that covers certain combinations, compare (bitwise AND or OR) these as a single CPU operation against each entity's flags (branchless logic), and get them into separate lists before doing any collision detection.

I would think in ECS I will check the components each body has and call the appropriate behavior, but of course the more behaviors I have the more it will cost, because each combination should be checked for each collision.

Any well-organised, easy-to-reason-about architecture incurs runtime performance overheads. If you employed some kind of AI to optimise much of the code that exists in the world today, you'd indeed get code that works well and lightning fast, but it would be totally unmaintainable by humans. (We may find in future that this might apply to AI software architects, as well.) Which do you prefer? I've had enough headaches on the job to know that maintainability is king in my book.

More generally, don't ECS add more cost when there are more behaviors ? In traditional way (OOP with inheritance), no matter how many different behaviours you have (the count of derived classes), you will always pay only one method call. In ECS, you update each system at each frame, so the more systems you have the more it will cost, even if there is no entities that use it.

In my experience of profiling, deeply nested conditionals are one of the main contributors to bad performance. Since most logic in a game engine is nested in one or more conditionals, wouldn't you rather keep conditional logic shallow? -- that part of what's a good ECS does.

Superficially, yes, more components means a higher cost over the same number of entities. But what matters is what happens within those blocks, and how deeply nested that (conditional) logic becomes. And remember, you don't have to process all of them every frame. This is true of either approach, ECS or regular.

Unlike typical Finite State Machines of about 15+ years ago (and some people still write code this way), ECS doesn't force you to go down a deep rabbithole of conditional blocks and method calls to reach the key logic you need to run in this moment, instead it says, "all things are pretty superficial, and we treat them largely the same". By superficial, I mean the main loop that runs through all components by all entities should be sitting somewhere quite near the root of your call-stack at all times. Anything beneath that should be fairly shallow.

If anything, smart list management (including maintaining updated lists through well-designed delta element add & remove approaches) is a far greater contributor to runtime performance.

Conclusion

Don't take a shallow view of the problem. Try it out yourself, experiment and profile within a bounded, well-defined spec. Application architectures are rarely simple things, so it's rarely possible to make simple assumptions about them, without concrete examples to talk about.

If you're worried about performance, spend your time wisely on choosing or implmenting a good ECS (don't struggle with deep inheritance hierarchies) and use the time that gains you to optimise sections of your ECS and other game logic.

\$\endgroup\$
0
\$\begingroup\$

There are good answers here, but I'll try to provide a short one:

If you have 100 or even 10'000 objects in game, it likely doesn't matter (much). When you're simulating 100k particles at 120fps, you will actually notice the differences between different data structures even within the same paradigm (OOP or ECS).

ECS is faster because it works closer to how CPUs and motherboards work best.

To understand why or when is ECS faster, you need to understand data structures (down to individual bytes), CPU microarchitectures, cache lines, L1 L2 L3 and RAM latency. A good starting point is the talk Scott Meyers: Cpu Caches and Why You Care.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .