Search Unity

How to organize job with unique sets of entities

Discussion in 'Entity Component System' started by fholm, Nov 15, 2018.

  1. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    I'm having problems with how to setup a job which requires several unique sets of entities. I'll explain a bit:

    1) I have 100 unique sets of entities and one component from each of the entities of varying size (these come from other part of the system, as Entity structs) .
    2) I need to serialize these 100 unique sets into a byte buffer per set.

    Solution 1 (doesn't work)

    My first instict was to use a IJobParallelFor structured like this:

    Code (csharp):
    1.  
    2.     struct JobParallel : IJobParallelFor {
    3.       public NativeArray<ByteBuffer> Buffers;
    4.       [ReadOnly] public NativeArray<NativeArray<PropertySet>> Storages;
    5.       public void Execute(int index) {
    6.         var buffer = Buffers[index];
    7.         var storage = Storages[index];
    8.         // iterate over storage, serialize into buffer
    9.       }
    10.     }
    11.  
    But this doesn't work, because you can't have nested native arrays.

    Solution 2 (does work, but is slow)

    My second idea was to use individual job instances, and queue 100 jobs.

    Code (csharp):
    1.  
    2.     struct JobSingle : IJob {
    3.       public ByteBuffer Buffer;
    4.       [ReadOnly] public NativeArray<PropertySet> Storage;
    5.       public void Execute() {
    6.         // iterate over Storage, serialize into Buffer
    7.       }
    8.     }
    9.  
    10.       protected override JobHandle OnUpdate(JobHandle handle) {
    11.         // ... code cut out here for brievty ...
    12.  
    13.         for (Int32 i = 0; i < _buffers.Length; ++i) {
    14.           JobSingle job;
    15.           job.Buffer = _buffers[i];
    16.           job.Storage = properties[i].;
    17.           handle = job.Schedule(handle);
    18.         }
    19.         return handle;
    20.       }
    21.  
    This does work, but it executes very very slowly (about 3-4x slower than it should from my other test), I think it's due to how the jobs are scheduled on the worker threads, as the profiler looks like this:

    upload_2018-11-15_14-25-23.png

    As far as I can tell only one job executes at a time, but on different worker threads, as you can't see any overlapping jobs executing? Why is this happening?

    Questions:

    1) How would I structure jobs like this? The sets of entities are unique per buffer and can't be found via regular filtering methods, etc.

    2) How do I make it so that the jobs execute safely when I basically have a HashSet<Entity> and need to pull components from the entities in it? I can't use the regular ComponentDataArray<T> or Chunks since these are usually a few 100-200 out of 50k+ entities in the world and each buffer needs a specific set of these entities serialized to it.

    Basically I have to do something like this to get the component data out of them:

    Code (csharp):
    1.  
    2.        HashSet<Entity> entites; // comes from somwhere else in the system
    3.         Int32 count = 0;
    4.         NativeArray<PropertySet> properties = new NativeArray<PropertySet>(entites.Count, Allocator.Temp);
    5.         foreach(var entity in entites) {
    6.           properties[count++] = EntityManager.GetComponentData<PropertySet>(entity);
    7.         }
    8.  
     
  2. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Your jobs are so tiny... Its questionable if it wouldn't be better to just run all of the work in a single job. This way it can still run in parallel to other game systems.
     
  3. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    The work the job does is (going to be) very heavy though, it doesn't have all of the logic in it yet. And I can't figure out a way to pass this data into a single job as doing it all in one job would mean that I need to pass a NativeArray<NativeArray<PropertySet>> which... doesn't work atm.

    Edit:

    This also has to run after all the other steps of the simulation is done (fixed time step/tick based), so I can't let it run freely with the rest of the systems.
     
  4. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    For Solution 2 the jobs should be executing in parallel. Can you try with [BurstCompile] if that makes them go in paralell.

    There are sometimes contention issues just getting mono to even run simple code like this...
     
  5. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    This is how it looks with the [BurstCompile] attribute (i artificially inflated the amount of work each job does to try to get them to execute in parallel but doesn't look like they want to).

    upload_2018-11-15_14-51-1.png

    Question: It says '377 Instances' even though I only start 100 instances... not quite sure why. I'm using _entityManager.CompleteAllJobs(); after i ran Update on all the systems in my tick simulation.

    Also I'm not sure the job will be burst compatible when it has all logic in it, as I have another job which uses the same underlaying structures and it crashes the editor when I try to burst compile it.
     
  6. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    I'll give some more information on exactly what I'm trying to do, basically:

    1) I have 100 clients connected to the game
    2) They all have X amount of entities in their area of interest
    3) The entities in their area of interest needs to be serialized into a packet to them

    All the example I've seen of the ECS jobs uses batch processing of massive amount entities, and no examples of trying to 'pick out' say 100-200 entities of a total set of tens of thousands.
     
  7. rjohnson06

    rjohnson06

    Joined:
    May 27, 2017
    Posts:
    17
    Aren't you creating a dependency on the previously scheduled job when you set and pass the handle like this on every iteration of the loop?

    handle = job.Schedule(handle);
     
  8. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    Hmm... Yeah maybe I am, but shouldn't it be able to figure out that the jobs dont collide and can run at the same time?
     
  9. M_R

    M_R

    Joined:
    Apr 15, 2015
    Posts:
    559
    no, if you pass the dependency you are telling the system to run sequentially.
    if you don't pass the dependency (and do
    JobHandle.CombineDependencies(...)
    afterwards) then the system checks if there are conflicts and throws

    automatic dependency handling is done between systems
     
  10. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    Okey so I've been trying to figure this out now... and i'm seeing some strange results. Been testing with IJob and IJobParallelFor, and IJobParallelFor outperforms IJob by 4x.

    If I use IJob and schedule 100 instances of the job, each job takes one packet buffer and one set of entities.

    If I use IJobParallelFor and pass an array of packet buffers, and an array of manually allocated buffers of entities, where each index in the array of packet buffer + array of entity buffers is what is supposed to be serialized together it's around 4x faster than IJob.

    The job they are doing is identical ... I would imagine that sure IJob is slightly less efficient, but 4x? No way.. something else must be going on.

    IJob is scheduled and completed like this:

    Code (csharp):
    1.  
    2.       var handleArray = new NativeArray<JobHandle>(100, Allocator.Persistent);
    3.  
    4.       // schedule all jobs
    5.       for (Int32 i = 0; i < _packets.Length; ++i) {
    6.         Job_IJob job;
    7.         job.Packet = _packets[i];
    8.         job.Entities = entitiesArrays[i];
    9.         handleArray[i] = job.Schedule(handle);
    10.       }
    11.  
    12.       // complete
    13.       JobHandle.CompleteAll(handleArray);
    14.  
    and IJobParallelFor is scheduled and completed like this:

    Code (csharp):
    1.  
    2.       Job_IJobParallelFor job;
    3.       job.Packets = _packets;
    4.       job.Entities = entitiesArrays;
    5.  
    6.       // schedule and complete
    7.       job.Schedule(_packets.Length, 8, handle).Complete();
    8.  
    Again, the code which execute inside of the jobs is identical, it calls into one static method like this for IJob:

    Code (csharp):
    1.  
    2.       public void Execute() {
    3.         WriteEntitiesIntoPacket(Packet, Entities);
    4.       }
    5.  
    And like this for IJobParallelFor:

    Code (csharp):
    1.  
    2.       public void Execute(Int32 index) {
    3.         WriteEntitiesIntoPacket(Packets[index], Entities[index]);
    4.       }
    5.  
    What the hell is going on? I would like to use the IJob if it's just slightly slower than IJobParallelFor because it means I don't have to blit over the entities into manually allocated buffers like I have to do to get them into the IJobParallelFor as you can't have nested NativeArray<T>

    Edit: In both cases the jobs do execute in parallel now.

    Here's screenshots of profiler with IJob:
    upload_2018-11-15_21-12-32.png

    Here's a screenshot of IJobParallelFor
    upload_2018-11-15_21-13-42.png
     
    Last edited: Nov 15, 2018
  11. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Enable burst, it will massively reduce specifically that overhead. Additionally profile in player vs editor. Since the job debugging system has significantly more overhead when having 100 jobs vs 1 IJobParallelFor with 100 iterations.

    Lastly its a totally unfair comparison. IJobParallelFor with 100 iterations is essentially as many IJob as there are worker threads and then an innerloop grabbing more and more indices until everything is done...
     
  12. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    You are right, in release mode build the difference is like... 10% maybe in total execution speed, with the benefit to IJobParallelFor

    Yeah it's absolutely not an apples to apples comparison, aware of that - it was not a performance comparison per say, but trying to find the fastest way that would let me do the work I need.
     
  13. Zooltan

    Zooltan

    Joined:
    Jun 14, 2013
    Posts:
    19

    Maybe you could flatten your storage array, so it's a single long NativeArray?

    Or you can use a NativeMultiHashMap to store your PropertySets? As long as it's [ReadOnly], it should work fine in IJobParallelFor.
     
  14. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    Hey!

    Thanks for the suggestions, but flattening the native array is not possible.. i'm going to look at the NativeMultiHashMap though, as that might be usable.. good idea.

    I have solved the problem for now, i am using a regular IJob, but i can schedule them without any dependancy on one another so they can all run 100% in parallel. My current system uses a lot of memory that i manually allocate to cut down on the copying I need to do between NativeArray<T>, etc. and it's really fast as I can process around 50k entities with fairly complex state on them in 0.3ms currently.