Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Allocating new NativeLists for each chunk inside an IJobChunk : good or bad?

Discussion in 'Entity Component System' started by PhilSA, Feb 16, 2020.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'd like to do something like this:
    Code (CSharp):
    1. [BurstCompile]
    2. struct ExampleJob : IJobChunk
    3. {
    4.     [ReadOnly]
    5.     public PhysicsWorld PhysicsWorld;
    6.     [ReadOnly]
    7.     public ArchetypeChunkComponentType<PhysicsCollider> PhysicsColliderType;
    8.     public ArchetypeChunkComponentType<Translation> TranslationType;
    9.     public ArchetypeChunkComponentType<Rotation> RotationType;
    10.  
    11.     public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    12.     {
    13.         NativeArray<PhysicsCollider> chunkPhysicsColliders = chunk.GetNativeArray(PhysicsColliderType);
    14.         NativeArray<Translation> chunkTranslations = chunk.GetNativeArray(TranslationType);
    15.         NativeArray<Rotation> chunkRotations = chunk.GetNativeArray(RotationType);
    16.  
    17.         // HERE: Allocate a new temporary NativeList for this chunk
    18.         NativeList<ColliderCastHit> tmpHits = new NativeList<ColliderCastHit>(8, Allocator.Temp);
    19.  
    20.         for (var i = 0; i < chunk.Count; i++)
    21.         {
    22.             PhysicsCollider physicsCollider = chunkPhysicsColliders[i];
    23.             Translation translation = chunkTranslations[i];
    24.             Rotation rotation = chunkRotations[i];
    25.  
    26.             // Do a bunch of logic/queries that use the NativeList
    27.             // ...
    28.             // ...
    29.             // ...
    30.  
    31.             // Write back
    32.             chunkTranslations[i] = translation;
    33.             chunkRotations[i] = rotation;
    34.         }
    35.  
    36.         // HERE: Dispose the temporary NativeList
    37.         tmpHits.Dispose();
    38.     }
    39. }

    This is an IJobChunk where for each chunk, we allocate a new NativeList before we iterate in the chunk, and dispose it after we're done iterating. Look for the comments that say "HERE: ". The general idea is to have each iteration in the chunk clear the list, fill it with results from physics queries, and compute something from the results in the list. I haven't tested this code yet but I'd like to know:
    • Is this legal?
    • Is "Allocator.Temp" the one I should be using?
    • Are there any pitfalls I should be aware of? Like for example;
      • Can a NativeList created in a job grow in size beyond its initial capacity?
      • Would it be way better to use NativeArray with max capacity instead?
    • Am I correct in assuming that I can safely read & write in that NativeList, even if the job is parallel? I guess everything in a given chunk necessarily lives on the same thread?
    • Right now I'm using a DynamicBuffer<HitBufferElement> on each entity to store hits instead of using the NativeList approach. Would I be right in assuming that the NativeList approach would at least not be significantly worse for performance than the DynamicBuffer approach?
     
    Last edited: Feb 16, 2020
  2. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,655
    No need in Dispose Temp allocator here :)
    Yes, I use this for some cases
    Inside job allocations - yes
    Yes it's in one thread per chunk
    Fallback (slow) allocators if total count of allocations overflow.
    I guess same perforamance, DB a bit faster when you work with them as NativeArrays, which fastest from all new Native* collections :)
     
    Last edited: Feb 16, 2020
    NotaNaN and PhilSA like this.
  3. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    The reason I wouldn't use dynamic buffers here is because it's just not correct encapsulation. You only need them in this specific job, but their scope is larger so another developer looking at it is going to be wondering where else is this used. To me that's more of an issue then performance differences that are likely white noise.

    Personally I would use Temp or maybe even fixed if the context allows for it. Use an allocator that fits the context best and other developers can reason about easily. Especially if this will be a third party asset. If testing or profiling proves that to be problematic,then go another route.
     
  4. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Reusing the same allocation during the job seems like a useful optimization.
    Depends on how many chunks are getting processed of course.

    Using an array is generally faster than NativeList, if it is possible to use.
    If all you really need is 8 elements then the absolute fastest is to use stackalloc.

    I would do this instead:

    Code (CSharp):
    1.  
    2. [BurstCompile]
    3. struct ExampleJob : IJobChunk
    4. {
    5.     [ReadOnly]
    6.     public PhysicsWorld PhysicsWorld;
    7.     [ReadOnly]
    8.     public ArchetypeChunkComponentType<PhysicsCollider> PhysicsColliderType;
    9.     public ArchetypeChunkComponentType<Translation> TranslationType;
    10.     public ArchetypeChunkComponentType<Rotation> RotationType;
    11.  
    12.     [NativeDisableContainerSafetyRestriction]
    13.     NativeList<ColliderCastHit> temporaryHits;
    14.  
    15.     public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    16.     {
    17.         NativeArray<PhysicsCollider> chunkPhysicsColliders = chunk.GetNativeArray(PhysicsColliderType);
    18.         NativeArray<Translation> chunkTranslations = chunk.GetNativeArray(TranslationType);
    19.         NativeArray<Rotation> chunkRotations = chunk.GetNativeArray(RotationType);
    20.  
    21.         // the job struct is created per thread, thus when the loop over all chunks in the IJobChunk outer loop
    22.         // begins this will allocate temp memory
    23.         // Allocator.Temp allocations will be automatically disposed when the job completes
    24.         if (!temporaryHits.IsCreated)
    25.             temporaryHits = new NativeList<ColliderCastHit>(8, Allocator.Temp);
    26.  
    27.         for (var i = 0; i < chunk.Count; i++)
    28.         {
    29.             PhysicsCollider physicsCollider = chunkPhysicsColliders[i];
    30.             Translation translation = chunkTranslations[i];
    31.             Rotation rotation = chunkRotations[i];
    32.  
    33.             // Do a bunch of logic/queries that use the NativeList
    34.             // ...
    35.             // ...
    36.             // ...
    37.  
    38.             // Write back
    39.             chunkTranslations[i] = translation;
    40.             chunkRotations[i] = rotation;
    41.         }
    42.     }
    43. }