Search Unity

The best way to combine a single threaded job from one system with other jobs / systems

Discussion in 'Entity Component System' started by Elfinnik159, Mar 3, 2021.

  1. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    Hello,

    I have a job that can only run on one thread. Because of this, the rest of the threads are idle. This jobs is very expensive. Therefore, I want to run other jobs in parallel that use other components. I know about CombineDependencies, but I cannot find a "nice solution".

    I want them to run in parallel. However, these systems have many Jobs. Each system is responsible for its part of the game (for its module).
    I can just combine them and use CombineDependencies. However, then I will get one system. This will increase optimization, but it will completely kill the modular approach, and the system may soon "turn into chaos". And I won't be able to use RequireForUpdate correctly.
    Is there a more correct way to do this? Or is this an acceptable sacrifice for the sake of such optimization?


    I have 2 systems:
    MinionActionSystem: controls the actions of units. ActionJob: For many reasons, it cannot be multithreaded (for example: in 1 frame, several units at once can increase the counter on another component). In addition to this Job, the system has ~ 20 others.
    NavMeshSystem: puts all units in the closest available location (used by NMQ.MapLocation). Multi-threaded. Has ~ 4 Jobs. All do not intersect ActionJob, but intersect with another Jobs in MinionActionSystem.
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,262
    The correct solution is to split up your systems into smaller systems where the more granular dependencies can be exposed to Unity's ECS layer. In my framework, I call ComponentSystemGroups SuperSystems and I call SystemBase SubSystems (these are subclasses) and I use that nomenclature exactly for the reason you describe (systems as modules). For sharing native containers (usually the problem people face when splitting systems), the SuperSystem can act as a hub, or I have a "collection component" mechanism which puts the burden of ownership onto entities instead.
     
  3. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    It sounds logical (I can probably also use SystemGroup), however... I have a large project with many different modules. And I am afraid that with this approach I will face the same problem, albeit to a lesser extent. Sort of:
    Minion_FindTarget => Minion_Move => Building _... => Minion_Orthers => NavMesh_SetLocation => Minion_ActionsJob
    And it turns out that the merger is not for its intended purpose, but for optimization. And just the system header is responsible for the assignment.
    However, this most likely means that I initially needed to plan the project and the execution queue with this in mind.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,262
    Can you clarify what you mean by this?

    Another option might just be hoping that some new modules you add down the line will fill that gap.
     
  5. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    Initially, when I was doing the project, I knew little about DOTS, but I tried to follow the advice from the articles or from the forum. So I just split the game into several modules. Each module is one system that is responsible for something.
    However, now, when the project has become large and I began to do optimization, I am faced with the fact that I have to literally mix everything together for the sake of optimization. For example, execute NavMeshPathfinding in parallel with other expensive job. NavMeshMapLocation is similar. And there are several such systems.
    In the end, I was able to achieve the fact that I have almost no "idle" in Job Workers. That is, all threads are running almost all the time. However, now I have lost modularity: Jobs are literally jumbled. And if earlier I had 10-20 Jobs in the system, then if I now group those that exist, I will get basically 1-4 Jobs per system.

    UPD:
    Oh, I forgot to mention probably an important part. I am making a multiplayer game in the hope that Burst determinism will be added soon. Therefore, all my systems are completely deterministic. Because of this, I am unable to use some of the multithreading capabilities (eg NativeContainer.ParallelWriter, etc.). So I have a lot of single threaded jobs (but fewer than multithreaded ones).
     
    Last edited: Mar 3, 2021
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,262
    Only the order of the scheduling of jobs and their dependencies from the main thread dictate determinism (with the exception of some of the ParallelWriter containers, which I also avoid). If the jobs don't run in the order they are scheduled, that means they are touching different data and the order doesn't matter. The result will be correct.

    So if you are mixing up the ordering of your systems, that suggests you have too many interdependencies between your systems. Breaking up the systems into smaller systems will help with that. But also you may need to evaluated your data design and possibly add some buffer components to help decouple your data dependencies.

    It's not "soon". That dropped quite a bit in priority likely because it is near-impossible to avoid a job scheduler in Mono-land to poison everything. Ints or soft floats are usually better answers.
     
  7. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    Sure. Although I "shuffled" the Jobs, they are still deterministic. Since only Jobs with a different set of components (or with the same [readonly]) are executed in parallel.

    Actually I have 4-5 systems that handle almost all of the components. This is what I mix with other expensive Jobs. The rest are much smaller.

    I have not seen this message. This is very sad. I started this project almost a year ago. I made a completely dedetermined system (on the same CPU architecture) in the hope that in a couple of months I can simply update Burst and the BurstCompile arguments will make my code fully determinate on all platforms. I'm ready for 2-3 times optimization degradation, but all Fixed libraries that I found (including in this forum section) in practice have 10+ times optimization degradation compared to float and math.

    In addition, in this case, I will have to write my own 3D physics, which will also be much slower than what we have now.
    However, if these limitations were initially known, I would do the project without physics.

    Are you using the deterministic Float library in your project?
     
    Last edited: Mar 4, 2021
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,262
    I don't understand why you are making your scheduling so ugly as an "optimization". That sounds like there's more fundamental issues at play because that shouldn't be necessary unless you are touching "hot" components like the transform components.

    No. I'm not targeting cross-platform determinism. I just use basic platform determinism for debugging and benchmarking purposes.
     
  9. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    A simple example:
    I have a Job that handles the actions of the units. For example, if action == Attack, it adds a "damage" event, if action == Mining, it gets the resource, if action == Put, it adds the resource to the warehouse. Etc. This Jobs handles ALL possible unit actions. And it cannot be multithreaded. For 2k units, it takes about 1ms. The rest of the threads are free at this moment. So I combined this Job with NavMeshPathfinding (multi-threaded). And now, in 1 ms, two Jobs are executed at once.

    The same example with hashing in NativeMultiHashMap: they cannot be multi-threaded, so I combine them together. But sometimes I have to combine them with other Jobs that use different components.