Skip to content

Performance Features

genar edited this page Nov 15, 2022 · 20 revisions

"Its not fast enough, i wanna simulate millions of entities, Arch sucks..." - No it does not, we got you ! Arch provides several features for especially those cases.

Bulk adding

Arch supports bulk adding of entities, this is incredible fast since it allows us to allocate enough space for a certain set of entities in one go. This reservation happens on top of the already existing entities in an archetype. You only need to reserve space once and than it will be filled later or sooner.

var archetype = new []{ typeof(Position), typeof(Velocity) };

// Create 1k entities
for(var index = 0; index < 1000; index++)
    world.Create(archetype)

world.Reserve(archetype, 1000000);              // Reserves space for additional 1mil entities
for(var index = 0; index < 1000000; index++)    // Create additional 1 mil entities
    world.Create(archetype)

// In total there now 1mil and 1k entities in that certain archetype. 

Highperformance Queries

The default Query API is easy to use and still very fast, perfect for fast prototyping and the most features of your game. However sometimes you need even more power, thats where the highperformance queries kick in.

world.HPQuery<Struct,T0,T1...>(in queryDescription, ref myStruct);
world.HPEQuery<Struct,T0,T1...>(in queryDescription, ref myEStruct);

Those highperformance queries use an interfacee and its struct implementation. This allows the compiler to inline the method call which results in less adress jumping and even faster iteration speed. Therefore you need to know two important interfaces and how to implement them.

public interface IForEach<T0...T10>{
    void Update(ref T0 t0, ref T1 t1, ... ref T10 t10);
}

public interface IForEachEntity<T0...T10>{
    void Update(in Entity entity, ref T0 t0, ref T1 t1, ... ref T10 t10);
}

Those two interfaces provide Update methods with various generic overloads which can be used to implement the entity operations. All you need to do is implementing the interface in a struct and passing that struct to the highperformance query api.

public struct VelocityUpdate : IForEach<Position, Velocity> {

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Update(ref Position pos, ref Velocity vel) { 
        pos.x += vel.x;
        pos.y += vel.y;
    }
}

world.HPQuery<VelocityUpdate, Position, Velocity>(in queryDescription);


// Also possible with a struct reference
public struct VelocityUpdate : IForEach<Position, Velocity> {

    public int counter;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Update(ref Position pos, ref Velocity vel) { 
        pos.x += vel.x;
        pos.y += vel.y;
        counter++;
    }
}

var velUpdate = new VelocityUpdate();
world.HPQuery<VelocityUpdate, Position, Velocity>(in queryDescription, ref velUpdate);
Console.WriteLine(velUpdate.counter);

Thats all, pretty cool right ? However in some cases you may also need a direct reference to the entity itself. In this case theres the IForEachEntity interface which is required.

public struct VelocityUpdate : IForEachEntity<Position, Velocity> {

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Update(in Entity entity, ref Position pos, ref Velocity vel) { 
        pos.x += vel.x;
        pos.y += vel.y;
        Console.WriteLine(entity);
    }
}

world.HPEQuery<VelocityUpdate, Position, Velocity>(in queryDescription);  // <- Requires HPEQuery instead of HPQuery

Multithreading

Still not fast enough ? You really want to simulate those million entities, dont you ? Well... i got your back !
Arch uses a selfwritten and alloc free JobScheduler under the hood to dispatch your query logic to a bunch of worker threads.

Before you can use those features you need to create an instance of the JobScheduler, this creates an singleton which the World will use.

var jobScheduler = new JobScheduler.JobScheduler("WorkerThread", threadAmountOptional);  // Is required to be initialized somewhere in your project
jobScheduler.Dispose();  // Once you exit your game/app

The easiest ways to make use of that multithreading are the Parallel Query overloads. The syntax is exactly the same as the normal Query methods you already know.

world.ParallelQuery(in query, ...);
world.HPParallelQuery(in query, ...);
world.HPEParallelQuery(in query, ...);

Therefore you can easily just rewrite your queries like this.

world.Query(in query, (ref Transform t, ref Velocity v) => {
   t.x += v.x;
   t.y += v.y;
});

to

world.ParallelQuery(in query, (ref Transform t, ref Velocity v) => {
   t.x += v.x;
   t.y += v.y;
});

And thats all ! The rest is handled under the hood, however those calls are blocking the mainthread. A parallel query is being scheduled to a bunch of worker-threads and the call waits for all scheduled jobs to finish before proceeding with the next query. Since the jobs are processed by multiple worker-threads simultaneous its incredible fast.

An alternative to those highlevel queries is the IChunkJob interface which can be passed to an specific overload. This inlined interface is then called by each worker thread for the processed chunks and you can define the logic yourself.

public struct VelocityUpdate : IChunkJob{

   public void Execute(int index, ref Chunk chunk) {
      
      var size = chunk.Size;
      var transforms = chunk.GetArray<Transform>();
      var velocities = velocity.GetArray<Velocity>();

      for(var index = 0; index < size; index++){

         var transform = transforms[index];
         ...
      }
   }
}

world.ParallelQuery(in query, new VelocityUpdate());

In multithreaded environment you should NEVER modify the world, archetype or chunk structure. You should not add or remove entities... however its totally fine to update entities.

Clone this wiki locally