3
\$\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\$
4
  • 1
    \$\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
    Mar 28, 2022 at 12:18
  • 1
    \$\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
    Mar 28, 2022 at 13:05
  • \$\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
    Mar 30, 2022 at 4:20
  • \$\begingroup\$ the optimisation is usualy done inside one frame, and than all others frame are otpimised. \$\endgroup\$
    – jon
    Oct 26, 2022 at 19:28

2 Answers 2

14
\$\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\$
3
\$\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\$

You must log in to answer this question.

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