Search Unity

For those who hate disposing NativeContainers

Discussion in 'Entity Component System' started by shuttler, Aug 17, 2018.

  1. shuttler

    shuttler

    Joined:
    Aug 17, 2018
    Posts:
    5
    We have all been there. We edited a job to include another NativeArray, and then went through the hassle of defining it outside the job and allocating it, then seeing no errors in VS, ran unity only to have a native array not disposed expection pop up a few seconds into playtesting or maybe only on the second run. It is just hard to remember to Dispose(), and find where the error is due to poor Exceptions.

    And even if you do remember, it's a hassle. It's boring. It doesn't add anything to the code, it just makes it long, unreadable, and hard to edit and iterate. It's hard to add and remove new Natives to jobs because the code necessary to make it work has to exist in at least three different areas: Declaration, Allocation, Disposal. It doesn't help that you have to redeclare Natives in multiple jobs if you want to pass them between them.

    And when the hell are we supposed to dispose TembJob allocations? You can't Dispose() them on the same OnUpdate call without creating unnecessary sync points with job.Complete(), so you really just store the jobHandle and complete it next frame or find some other solution (which is a hassle). And at least in preview-8 the attribute [DeallocateOnJobCompletion] isn't available for all Natives.

    The thing is, Natives violate a top programming principle of mine: which is to be lazy.


    So then I thought, when do the Allocator types want to be Disposed? And really it is already given. Persistents want to be destroyed at OnDestroyManager. TempJobs want to be disposed by at most frame four, but really can be disposed together with Temps at the end of the frame (or one frame later).

    So I used a few days to create and refine a NativeManagerSystem which handles allocation and disposal for you, because I am lazy (Is that contradictory?).

    How it's used:
    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Jobs;
    5. using Unity.Transforms2D;
    6.  
    7. [AlwaysUpdateSystem]
    8. public class TestSystem : JobComponentSystem
    9. {
    10.     struct Points
    11.     {
    12.         public readonly int Length;
    13.         public ComponentDataArray<Position2D> positions;
    14.     }
    15.     [Inject] Points points;
    16.  
    17.     [BurstCompile]
    18.     struct SimpleJob : IJob
    19.     {
    20.         public ComponentDataArray<Position2D> positions;
    21.  
    22.         public NativeArray<int> array;
    23.         public NativeList<int> list;
    24.         public NativeQueue<int> queue;
    25.         public NativeHashMap<int, int> hashMap;
    26.         public NativeMultiHashMap<int, int> multiHashMap;
    27.  
    28.         public void Execute()
    29.         {
    30.             positions[0] = new Position2D() { Value = positions[0].Value };
    31.             array[0] = 1;
    32.             list.Add(1);
    33.             queue.Enqueue(1);
    34.             hashMap.TryAdd(0, 1);
    35.             multiHashMap.Add(0, 1);
    36.         }
    37.     }
    38.     [Inject] NativeManagerSystem manager;
    39.     NativeList<int> persistentList;
    40.  
    41.     protected override void OnCreateManager(int capacity)
    42.     {
    43.         persistentList = manager.AllocateAndAutoDisposeNativeList<int>(Allocator.Persistent);
    44.     }
    45.  
    46.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    47.     {
    48.         var array = manager.AllocateAndAutoDisposeNativeArray<int>(1, Allocator.Temp);
    49.         var queue = manager.AllocateAndAutoDisposeNativeQueue<int>( Allocator.TempJob);
    50.  
    51.         var job = new SimpleJob() { positions = points.positions };
    52.  
    53.         manager.AllocateAndAutoDispose(ref job.array, points.Length, Allocator.TempJob);
    54.         manager.AllocateAndAutoDispose(ref job.queue, Allocator.TempJob);
    55.         manager.AllocateAndAutoDispose(ref job.list, Allocator.TempJob);
    56.         manager.AllocateAndAutoDispose(ref job.hashMap, 10, Allocator.TempJob);
    57.         manager.AllocateAndAutoDispose(ref job.multiHashMap, 10, Allocator.TempJob);
    58.        
    59.         inputDeps = job.Schedule(inputDeps);
    60.  
    61.         return inputDeps;
    62.     }
    63. }
    64.  
    The system which you can copy and use if you hate Disposing as much as I do:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Unity.Entities;
    4.  
    5. namespace Unity.Collections
    6. {
    7.     [AlwaysUpdateSystem]
    8.     [UpdateAfter(typeof(EndFrameBarrier))]
    9.     public class NativeManagerSystem : ComponentSystem
    10.     {
    11.         private Queue<IDisposable> tempJobDisposeQueue = new Queue<IDisposable>();
    12.  
    13.         private Queue<IDisposable> persistentDisposeQueue = new Queue<IDisposable>();
    14.  
    15.         private Queue<IDisposable> disposableProcessQueue = new Queue<IDisposable>();
    16.         private Queue<Allocator> allocatorProcessQueue = new Queue<Allocator>();
    17.  
    18.         private void EnqueueNative(IDisposable native, Allocator allocator)
    19.         {
    20.             disposableProcessQueue.Enqueue(native);
    21.             allocatorProcessQueue.Enqueue(allocator);
    22.         }
    23.        
    24.         protected override void OnUpdate()
    25.         {
    26.             EntityManager.CompleteAllJobs();
    27.             DisposeTempJobs();
    28.             ProcessDisposables();
    29.         }
    30.  
    31.         protected override void OnDestroyManager()
    32.         {
    33.             EntityManager.CompleteAllJobs();
    34.             ProcessDisposables();
    35.             DisposeTempJobs();
    36.             DisposePersistents();
    37.         }
    38.  
    39.         private void DisposeTempJobs()
    40.         {
    41.             int count = tempJobDisposeQueue.Count;
    42.             for (int i = 0; i < count; i++)
    43.             {
    44.                 tempJobDisposeQueue.Dequeue().Dispose();
    45.             }
    46.         }
    47.  
    48.         private void DisposePersistents()
    49.         {
    50.             int count = persistentDisposeQueue.Count;
    51.             for (int i = 0; i < count; i++)
    52.             {
    53.                 persistentDisposeQueue.Dequeue().Dispose();
    54.             }
    55.         }
    56.  
    57.         private void ProcessDisposables()
    58.         {
    59.             int count = disposableProcessQueue.Count;
    60.             for (int i = 0; i < count; i++)
    61.             {
    62.                 var manager = disposableProcessQueue.Dequeue();
    63.                 var allocator = allocatorProcessQueue.Dequeue();
    64.  
    65.                 switch (allocator)
    66.                 {
    67.                     case Allocator.Temp:
    68.                         manager.Dispose();
    69.                         break;
    70.                     case Allocator.TempJob:
    71.                         tempJobDisposeQueue.Enqueue(manager);
    72.                         break;
    73.                     case Allocator.Persistent:
    74.                         persistentDisposeQueue.Enqueue(manager);
    75.                         break;
    76.                 }
    77.             }
    78.         }
    79.  
    80.         public void AllocateAndAutoDispose<T>(ref NativeArray<T> native, int length, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) where T : struct
    81.         {
    82.             native = new NativeArray<T>(length, allocator, options);
    83.             EnqueueNative(native, allocator);
    84.         }
    85.         public void AllocateAndAutoDispose<T>(ref NativeQueue<T> native, Allocator allocator) where T : struct
    86.         {
    87.             native = new NativeQueue<T>(allocator);
    88.             EnqueueNative(new NativeQueueWrapper<T>(native), allocator);
    89.         }
    90.         public void AllocateAndAutoDispose<T1, T2>(ref NativeHashMap<T1, T2> native, int capacity, Allocator allocator) where T1 : struct, IEquatable<T1> where T2 : struct
    91.         {
    92.             native = new NativeHashMap<T1, T2>(capacity, allocator);
    93.             EnqueueNative(new NativeHashMapWrapper<T1, T2>(native), allocator);
    94.         }
    95.         public void AllocateAndAutoDispose<T1, T2>(ref NativeMultiHashMap<T1, T2> native, int capacity, Allocator allocator) where T1 : struct, IEquatable<T1> where T2 : struct
    96.         {
    97.             native = new NativeMultiHashMap<T1, T2>(capacity, allocator);
    98.             EnqueueNative(new NativeMultiHashMapWrapper<T1, T2>(native), allocator);
    99.         }
    100.         public void AllocateAndAutoDispose<T>(ref NativeList<T> native, int capacity, Allocator allocator) where T : struct
    101.         {
    102.             native = new NativeList<T>(capacity, allocator);
    103.             EnqueueNative(native, allocator);
    104.         }
    105.         public void AllocateAndAutoDispose<T>(ref NativeList<T> native, Allocator allocator) where T : struct
    106.         {
    107.             native = new NativeList<T>(allocator);
    108.             EnqueueNative(native, allocator);
    109.         }
    110.  
    111.         public NativeArray<T> AllocateAndAutoDisposeNativeArray<T>(int length, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) where T : struct
    112.         {
    113.             var native = new NativeArray<T>(length, allocator);
    114.             EnqueueNative(native, allocator);
    115.             return native;
    116.         }
    117.  
    118.         public NativeQueue<T> AllocateAndAutoDisposeNativeQueue<T>(Allocator allocator) where T : struct
    119.         {
    120.             var native = new NativeQueue<T>(allocator);
    121.             EnqueueNative(new NativeQueueWrapper<T>(native), allocator);
    122.             return native;
    123.         }
    124.  
    125.         public NativeHashMap<T1, T2> AllocateAndAutoDisposeNativeHashMap<T1, T2>(int capacity, Allocator allocator)
    126.             where T1 : struct, IEquatable<T1>
    127.             where T2 : struct
    128.         {
    129.             var native = new NativeHashMap<T1, T2>(capacity, allocator);
    130.             EnqueueNative(new NativeHashMapWrapper<T1, T2>(native), allocator);
    131.             return native;
    132.         }
    133.  
    134.         public NativeMultiHashMap<T1, T2> AllocateAndAutoDisposeNativeMultiHashMap<T1, T2>(int capacity, Allocator allocator)
    135.             where T1 : struct, IEquatable<T1>
    136.             where T2 : struct
    137.         {
    138.             var native = new NativeMultiHashMap<T1, T2>(capacity, allocator);
    139.             EnqueueNative(new NativeMultiHashMapWrapper<T1, T2>(native), allocator);
    140.             return native;
    141.         }
    142.  
    143.         public NativeList<T> AllocateAndAutoDisposeNativeList<T>(int capacity, Allocator allocator) where T : struct
    144.         {
    145.             var native = new NativeList<T>(capacity, allocator);
    146.             EnqueueNative(native, allocator);
    147.             return native;
    148.         }
    149.  
    150.         public NativeList<T> AllocateAndAutoDisposeNativeList<T>(Allocator allocator) where T : struct
    151.         {
    152.             var native = new NativeList<T>(allocator);
    153.             EnqueueNative(native, allocator);
    154.             return native;
    155.         }
    156.  
    157.        
    158.  
    159.         public struct NativeQueueWrapper<T> : IDisposable where T : struct
    160.         {
    161.             private NativeQueue<T> queue;
    162.  
    163.             public NativeQueueWrapper(NativeQueue<T> queue)
    164.             {
    165.                 this.queue = queue;
    166.             }
    167.             public void Dispose()
    168.             {
    169.                 queue.Dispose();
    170.             }
    171.         }
    172.  
    173.         public struct NativeHashMapWrapper<T1, T2> : IDisposable where T1 : struct, IEquatable<T1> where T2 : struct
    174.         {
    175.             private NativeHashMap<T1, T2> hashMap;
    176.  
    177.             public NativeHashMapWrapper(NativeHashMap<T1, T2> hashMap)
    178.             {
    179.                 this.hashMap = hashMap;
    180.             }
    181.             public void Dispose()
    182.             {
    183.                 hashMap.Dispose();
    184.             }
    185.         }
    186.  
    187.         public struct NativeMultiHashMapWrapper<T1, T2> : IDisposable where T1 : struct, IEquatable<T1> where T2 : struct
    188.         {
    189.             private NativeMultiHashMap<T1, T2> hashMap;
    190.  
    191.             public NativeMultiHashMapWrapper(NativeMultiHashMap<T1, T2> hashMap)
    192.             {
    193.                 this.hashMap = hashMap;
    194.             }
    195.             public void Dispose()
    196.             {
    197.                 hashMap.Dispose();
    198.             }
    199.         }
    200.     }
    201. }
    202.  
     
  2. starikcetin

    starikcetin

    Joined:
    Dec 7, 2017
    Posts:
    340
    This looks like a primitive garbage collector.
     
  3. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    The attribute has not disappeared anywhere and we actively used it as in preview.8 before and in the preview.10 now

    upload_2018-8-18_8-1-16.png
     
    Last edited: Aug 18, 2018
  4. shuttler

    shuttler

    Joined:
    Aug 17, 2018
    Posts:
    5
    Yes I know it exists and works some of the Natives like NativeArray. But at least for Unity 2.0f, preview-8 which I am using, it isn't supported for Native Hashmaps /Multihashmaps.

    It is very likely that that has been fixed in the next preview, but since it isn't available in the package manager for me and I've read that it breaks things, I haven't updated yet. Or perhaps there is a more up to date preview-8 if I install the newest unity build?
     
  5. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    I am curious what the error says when you use the attribute on native hash map?
    Good way to find the change of feature is to search for the error message in the source code and look around it.
     
  6. shuttler

    shuttler

    Joined:
    Aug 17, 2018
    Posts:
    5
    InvalidOperationException: SimpleJob.hashMap uses [DeallocateOnJobCompletion] but the native container does not support deallocation of the memory from a job.

    Also: Please make all NativeContainers IDisposable, so then I don't need to make wrappers for them.
     
    5argon likes this.
  7. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    preview.10 allowed in stugging packages, It's not breaks it's evolve. Watch difference in my thread https://forum.unity.com/threads/preview-8-preview-10-difference.545734.
     
  8. shuttler

    shuttler

    Joined:
    Aug 17, 2018
    Posts:
    5
  9. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
  10. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I don't know what to say beyond that this is a bad idea. The whole point of explicit memory management is that memory is a very precious resource that should be tightly controlled and explicit, so its visible.

    That is performance by default.

    And you are using GC memory to track natively allocated memory, thats double bad.

    Is it really that much work to type Dispose in OnDestroyManager? Maybe its better to just get used to it?
     
  11. deplinenoise

    deplinenoise

    Unity Technologies

    Joined:
    Dec 20, 2017
    Posts:
    33
    What Joachim said, with one addition:

    I do agree it is annoying to only get runtime errors for mistakes with buffer management. Once the dust settles we'll want to look into ways of making most common mistakes compile time errors via custom compiler extensions.
     
  12. shuttler

    shuttler

    Joined:
    Aug 17, 2018
    Posts:
    5

    Well I'm not the kind of person to believe something without evidence, so I took the time to test the time cost of the overhead that comes with NativeManagerSystem.

    Allocating 10000 NativeArrays every frame, each with length of 10000

    Allocator.Temp:

    Using NativeManagerSystem: ~820ms
    To allocate and store in dispose queue: ~720 ms per frame
    To dispose on end of frame: ~100 ms per frame

    Without using NativeManagerSystem: ~330 ms
    To allocate and dispose on same frame: ~330 ms per frame



    Allocator.TempJob:

    Control test:
    Scheduling and completing 10000 jobs every frame: ~430 ms per frame

    Using NativeManagerSystem: ~1450ms
    To allocate and store in dispose queue and schedule and complete: ~1350 ms per frame
    To dispose every frame: ~110 ms per frame

    Without using NativeManagerSystem: ~1050ms
    To allocate and schedule and complete with [DeallocateOnJobCompletion]: ~1050 ms per frame

    Code (CSharp):
    1.  
    2. public class Toggle
    3. {
    4.     public static bool useManager = false;
    5.     public static int size = 10000;
    6.     public static int repetitions = 10000;
    7. }
    8.  
    9. [AlwaysUpdateSystem]
    10. public class TestTempArraySystem : JobComponentSystem
    11. {
    12.     [Inject] NativeManagerSystem manager;
    13.  
    14.  
    15.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    16.     {
    17.         for (int i = 0; i < Toggle.repetitions; i++)
    18.         {
    19.             if (Toggle.useManager)
    20.             {
    21.                 var native = manager.AllocateAndAutoDisposeNativeArray<int>( Toggle.size, Allocator.Temp);
    22.             }
    23.             else
    24.             {
    25.                 var native = new NativeArray<int>(Toggle.size, Allocator.Temp);
    26.                 native.Dispose();
    27.             }
    28.         }
    29.         return inputDeps;
    30.     }
    31. }
    32.  
    33. [AlwaysUpdateSystem]
    34. public class TestArrayWithJobSystem : JobComponentSystem
    35. {
    36.     [Inject] NativeManagerSystem manager;
    37.  
    38.     struct DeallocationJob : IJob
    39.     {
    40.         [DeallocateOnJobCompletion]
    41.         public NativeArray<int> array;
    42.  
    43.         public void Execute() { }
    44.     }
    45.     struct NoDeallocationJob : IJob
    46.     {
    47.         public NativeArray<int> array;
    48.  
    49.         public void Execute() { }
    50.     }
    51.  
    52.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    53.     {
    54.         for (int i = 0; i < Toggle.repetitions; i++)
    55.         {
    56.             if (Toggle.useManager)
    57.             {
    58.                 var job = new NoDeallocationJob();
    59.                 manager.AllocateAndAutoDispose(ref job.array, Toggle.size, Allocator.TempJob);
    60.                 job.Schedule().Complete();
    61.             }
    62.             else
    63.             {
    64.                 var job = new DeallocationJob();
    65.                 job.array = new NativeArray<int>(Toggle.size, Allocator.TempJob);
    66.                 job.Schedule().Complete();
    67.             }
    68.         }
    69.  
    70.         return inputDeps;
    71.     }
    72. }
    73.  
    74. [AlwaysUpdateSystem]
    75. public class TestJobSystem : JobComponentSystem
    76. {
    77.     [Inject] NativeManagerSystem manager;
    78.  
    79.     struct SimpleJob : IJob
    80.     {
    81.         public void Execute() { }
    82.     }
    83.  
    84.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    85.     {
    86.         for (int i = 0; i < Toggle.repetitions; i++)
    87.         {
    88.             var job = new SimpleJob();
    89.             job.Schedule().Complete();
    90.         }
    91.  
    92.         return inputDeps;
    93.     }
    94. }

    So yes I agree, there definitely is cost to garbage collect NativeArrays, and is therefore generally a bad idea if you want max performance.

    However I still believe that code readability and editability is of great importance. And generally the longer your code is, the less readable it is. Having many lines of Dispose() calls, though easy to read and gloss over, still takes up valuable screen space that could have been filled with important lines of code. But perhaps for performant code, I could live with it.

    But the real problem I had was with adding and removing TempJob NativeMultiHashmaps from code. Because [DeallocateOnJobCompletion] is not supported for them, I had to manually find a way to Dispose() them at the right time. And the first thing I came up with was to declare it as a field in the ComponentSystem and Job, Allocate in OnCreateManager(), Dispose() and Allocate in OnUpdate(), and finally Dispose() in OnDestroyManager(). Which together with transferring the field over to the job makes a total of seven places I have to visit if I want to add or remove a TempJob Multihashmap.

    But the funny thing is it was not performant at all, because allocating a TempJob NativeHashmap or NativeMultiHashmap is super slow. It takes ~4700 ms to allocate and dispose 10000 NativeHashmaps each with capacity 10000 (same for multihashmaps). I don't know if I'm seriously underestimating how much memory a hashmap requires, or if there is something wrong with the allocation implementation, but that is pretty slow for a "performance by default" NativeContainer.

    So in the end it was faster and more code-space efficient (only 5 places to visit) to create a Persistent native hashmap and clear it every frame in a burst-compiled job (which was still relatively slow to all of my other jobs). Now in hindsight I can make it even easier to add/remove Persistents, just by saving the job with the Persistent as a field and scheduling it every frame, which makes only three places to visit.

    Code (CSharp):
    1. public class TestJobSystem : JobComponentSystem
    2. {
    3.     SimpleJob job;
    4.     JobHandle jobHandle;
    5.  
    6.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    7.     {
    8.         jobHandle.Complete();
    9.         jobHandle = job.Schedule(inputDeps);
    10.  
    11.         return jobHandle;
    12.     }
    13.  
    14.     struct SimpleJob : IJob
    15.     {
    16.         public NativeArray<int> array;
    17.         public void Execute() { }
    18.     }
    19.     protected override void OnCreateManager(int capacity)
    20.     {
    21.         job = new SimpleJob();
    22.         job.array = new NativeArray<int>(10000, Allocator.Persistent);
    23.     }
    24.     protected override void OnDestroyManager()
    25.     {
    26.         job.array.Dispose();
    27.     }
    28. }

    So in conclusion, I realize that I can deal with disposing Persistents and Temps, because I can minimize the places to visit. And with [DeallocateOnJobCompletion], TempJobs can be just as code-space efficient. The real issue here is NativeHashmaps and NativeMultiHashmaps, because they are slow. But now with buffer arrays, I think I will be able to avoid needing them.

    (Sorry for the long essay)
     
  13. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    So yes, we agree the current allocator system [DeallocateOnJobCompletion] is not ideal. The inability to group allocators together and just wipe everything is not ideal either. We will get back to that.

    But this is not the solution :)

    Also in the above, please measure in the player. In the editor you are paying for significant safety overhead. That goes away in the player, and you will truly see how massive the overhead you are adding really is...
     
  14. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,769
    I don't think adding single line of dispose, is a big issue. Nor, if I need few dispose lines in a method.
    It may be nice if is done automatically, but if focusing on performance using ECS (the main point), shouldn't waste it (i,e, GC).