Multithreading support in Entity Component System

Sometimes we have alot of data with complex processing logic and it takes alot of cpu time. Modern cpus have multiple cores and code execution can be splited between them for parallel data processing in same time. How we can get performance boost inside ECS framework systems without huge code changes?

Sometimes we have alot of data with complex processing logic and it takes alot of cpu time. Modern cpus have multiple cores and code execution can be splited between them for parallel data processing in same time. How we can get performance boost inside ECS framework systems without huge code changes?

So, sounds easy, spawn new background threads and send some data to them, then get it back. But there are some problems.

Problems

Synchronization of read / write operation over data

As solution, we can split data to isolated chunks of data and send each chunk to threaded worker - then we can read data without any synchronization. For same reason we can’t emit any events for adding / removing / updating components data - with write synchronization it will get performance penalty.

How to get results of thread data processing in main thread

As solution we can block main thread and wait when all workers will finish data processing. It should works good enough in current ECS implementation - multithreaded system can be standard IEcsRunSystem and works as part of any EcsSystems group. As option we should be able to not synchronize results right now, but do it later somehow.

Data amount can’t be processed in same time with fixed job size

There are 2 cases:

  • Worker can process fixed max amount of entities. If there are additional data and no free workers, job broker just locks main thread and waits when any worker will be freed.
  • All entities will be splitted between fixed amount of threads. It can be useful for decreasing main thread locks, but dont allows to control execution time of each worker.

Now first case was implemented, but looks like second one will work much better.

Example

Any multithreaded system should be inherited from EcsMultiThreadSystem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class TestMultiThreadSystem : EcsMultiThreadSystem {
[EcsWorld]
EcsWorld _world;

[EcsFilterInclude (typeof (C12_1))]
EcsFilter _filter;

protected override EcsFilter GetFilter () {
return _filter;
}

protected override EcsWorld GetWorld () {
return _world;
}

protected override Action<EcsMultiThreadJob> GetWorker () {
return ProcessJob;
}

protected override int GetThreadsCount () {
return 4;
}

protected override int GetJobSize () {
return 100000;
}

protected override bool GetForceSyncState () {
return true;
}

static void ProcessJob (EcsMultiThreadJob job) {
for (var i = job.From; i < job.To; i++) {
var c = job.World.GetComponent<C12_1> (job.Entities[i]);
// processing.
}
}
}

Performance tests

Conditions

We will test 3 similar systems with same logic:

  • Standard IEcsRunSystem system
  • EcsMultiThreadSystem system with 4 threads
  • EcsMultiThreadSystem system with 8 threads

Results

  • Standard IEcsRunSystem system: 532ms
  • EcsMultiThreadSystem system with 4 threads: 118ms
  • EcsMultiThreadSystem system with 8 threads: 34ms

Looks good, but can be better with splitting all data between fixed amount of threads instead of working with fixed job size. This can be changed in next commits, stay tuned!