Skip to main content
added 15 characters in body
Source Link
Philipp
  • 121.5k
  • 28
  • 261
  • 342
  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-componentcomponents. The healcheck-if-dead-loop only requires the health-componentcomponents, 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.
  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-component. The heal-loop only requires the health-component, 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 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.
  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.
added 78 characters in body
Source Link
Philipp
  • 121.5k
  • 28
  • 261
  • 342

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

Why is the second usually faster?

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

added 223 characters in body
Source Link
Philipp
  • 121.5k
  • 28
  • 261
  • 342

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)
   ifswitch entity canattacking shoottype
       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)

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-component. The heal-loop only requires the health-component, 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 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.

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)
   if entity can shoot
        shoot(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 has health
    checkIfDead(entity)

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-component. The heal-loop only requires the health-component, 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 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.

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)

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-component. The heal-loop only requires the health-component, 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 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.
Source Link
Philipp
  • 121.5k
  • 28
  • 261
  • 342
Loading