Search Unity

  1. The Unity Pro & Visual Studio Professional Bundle gives you the tools you need to develop faster & collaborate more efficiently. Learn more.
    Dismiss Notice
  2. Improved Prefab workflow (includes Nested Prefabs!), 2D isometric Tilemap and more! Get the 2018.3 Beta now.
    Dismiss Notice
  3. Want more efficiency in your development work? Sign up to receive weekly tech and creative know-how from Unity experts.
    Dismiss Notice
  4. Participate with students all over the world and build projects to teach people. Join now!
    Dismiss Notice
  5. Build games and experiences that can load instantly and without install. Explore the Project Tiny Preview today!
    Dismiss Notice
  6. Improve your Unity skills with a certified instructor in a private, interactive classroom. Watch the overview now.
    Dismiss Notice
  7. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

Batch EntityCommandBuffer

Discussion in 'Entity Component System and C# Job system' started by tertle, Dec 6, 2018 at 1:00 AM.

  1. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    698
    So I read @5argon blog post over here this morning https://gametorrahod.com/unity-ecs-batched-operation-on-entitymanager-3c35e8e5ecf4

    And as I use quite a few short lived event entities in my project I was curious if I could write something that could generically replaced EntityCommandBuffer for these. So I did!

    Firstly code, this is my first, not well tested and quickly written proof of concept.

    -edit- dramatically updating code, will post it when i'm done.
    -edit2- latested post here, now 3x faster than original version (10x faster than a barrier): https://forum.unity.com/threads/batch-entitycommandbuffer.593569/#post-3965998

    How it works. It passes a NativeQueue<T> whenever requested.

    Get the barrier with
    this.batchBarrier = this.World.GetOrCreateManager<BatchBarrierSystem>();
    The same barrier is used for all systems.

    To create an entity use

    NativeQueue<T>.Concurrent createQueue = this.batchBarrier.GetCreateQueue<T>().ToConcurrent();


    where T is an IComponentData that will be added and set on the entity.

    Then just pass it to a job, for example

    Code (CSharp):
    1.         [BurstCompile]
    2.         private struct CreateEntitiesJob : IJobParallelFor
    3.         {
    4.             public NativeQueue<TestComponent>.Concurrent AddComponentQueue;
    5.  
    6.             /// <inheritdoc />
    7.             public void Execute(int index)
    8.             {
    9.                 this.AddComponentQueue.Enqueue(new TestComponent { Index = index });
    10.             }
    11.         }
    To destroy an entity, use the

    NativeQueue<Entity>.Concurrent = this.batchBarrier.GetDestroyQueue().ToConcurrent(),


    Any entity added to this queue will be destroyed.

    Code (CSharp):
    1. [BurstCompile]
    2.         private struct DestroyEntitiesJob : IJobProcessComponentDataWithEntity<TestComponent>
    3.         {
    4.             public NativeQueue<Entity>.Concurrent DestroyEntityQueue;
    5.  
    6.             /// <inheritdoc />
    7.             public void Execute(Entity entity, int index, ref TestComponent data)
    8.             {
    9.                 this.DestroyEntityQueue.Enqueue(entity);
    10.             }
    11.         }
    The one extra bit you need to do atm is pass the dependency to the system because I don't have any access to AfterUpdate();

    Code (CSharp):
    1. this.batchBarrier.AddDependency(handle);
    2.  
    3. return handle;
    Why bother?

    At the moment it is about 3x faster than using EntityCommandBuffers and completely garbage free.
    Also it works in Burst jobs

    I believe I could probably optimize it quite a bit if I limit the use.

    100,000 entities being added and removed each frame.
    This is done in a build (with profiler attached obviously)
    I'm not sure why endframebarrier is throwing garbage.

    upload_2018-12-6_11-56-26.png

     
    Last edited: Dec 6, 2018 at 4:02 AM
    eizenhorn and 5argon like this.
  2. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    698
    upload_2018-12-6_12-19-13.png

    upload_2018-12-6_12-19-55.png

    upload_2018-12-6_12-20-11.png

    Significant performance increase if I force these entities to only live 1 frame (also saves you having to clean them up myself). Has become more like a dedicated event system, which I kind of like.
     
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    698
    Moving the SetComponent to a job significantly improves performance (renamed to EventSystem though that kind of clashes with the unity ui system so i'll probably change that)

    upload_2018-12-6_14-59-8.png

    Thats 100k entities created in 3.4ms on a pretty old 3570k, vs 34ms for a barrier.

    I can only think of 1 more optimization that needs doing now, and that's making different types set in parallel (won't affect this benchmark results as they are only a single type test)
     
    Last edited: Dec 6, 2018 at 4:06 AM
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    698
    No longer has a destroy method. Entities will live 1 frame exactly. Destroying entity is now done automatically.

    Source code here:

    Code (CSharp):
    1.  
    2. // <copyright file="BatchBarrierSystem.cs" company="Timothy Raines">
    3. //     Copyright (c) Timothy Raines. All rights reserved.
    4. // </copyright>
    5.  
    6. namespace BovineLabs.Common.Native
    7. {
    8.     using System;
    9.     using System.Collections.Generic;
    10.     using Unity.Burst;
    11.     using Unity.Collections;
    12.     using Unity.Entities;
    13.     using Unity.Jobs;
    14.  
    15.     /// <summary>
    16.     /// The BatchBarrierSystem.
    17.     /// </summary>
    18.     public sealed class EntityEventSystem : ComponentSystem
    19.     {
    20.         private readonly Dictionary<Type, IBatcher> types = new Dictionary<Type, IBatcher>();
    21.  
    22.         private JobHandle dependencies = default;
    23.  
    24.         /// <inheritdoc />
    25.         protected override void OnDestroyManager()
    26.         {
    27.             foreach(var t in this.types)
    28.             {
    29.                 t.Value.Dispose();
    30.             }
    31.  
    32.             this.types.Clear();
    33.         }
    34.  
    35.         /// <inheritdoc />
    36.         protected override void OnUpdate()
    37.         {
    38.             this.dependencies.Complete();
    39.             this.dependencies = default;
    40.  
    41.             foreach (var t in this.types)
    42.             {
    43.                 t.Value.Update(this.EntityManager);
    44.             }
    45.         }
    46.  
    47.         public NativeQueue<T> GetBatcher<T>()
    48.             where T : struct, IComponentData
    49.         {
    50.             if (!this.types.TryGetValue(typeof(T), out var create))
    51.             {
    52.                 create = this.types[typeof(T)] = new Batcher<T>(this.EntityManager);
    53.             }
    54.  
    55.             return ((Batcher<T>)create).GetNew();
    56.         }
    57.  
    58.         public void AddDependency(JobHandle handle)
    59.         {
    60.             this.dependencies = JobHandle.CombineDependencies(this.dependencies, handle);
    61.         }
    62.  
    63.         private interface IBatcher : IDisposable
    64.         {
    65.             void Update(EntityManager entityManager);
    66.         }
    67.  
    68.         private class Batcher<T> : IBatcher
    69.             where T : struct, IComponentData
    70.         {
    71.             private readonly EntityArchetype archetype;
    72.             private readonly List<NativeQueue<T>> queues = new List<NativeQueue<T>>();
    73.  
    74.             private NativeArray<Entity> entities;
    75.             private EntityArchetypeQuery query;
    76.  
    77.             public Batcher(EntityManager entityManager)
    78.             {
    79.                 this.archetype = entityManager.CreateArchetype(typeof(T));
    80.  
    81.                 this.query = new EntityArchetypeQuery
    82.                 {
    83.                     Any = Array.Empty<ComponentType>(),
    84.                     None = Array.Empty<ComponentType>(),
    85.                     All = new[] { ComponentType.Create<T>() },
    86.                 };
    87.             }
    88.  
    89.             public int GetCount()
    90.             {
    91.                 var sum = 0;
    92.                 foreach (var i in this.queues)
    93.                 {
    94.                     sum += i.Count;
    95.                 }
    96.  
    97.                 return sum;
    98.             }
    99.  
    100.             public NativeQueue<T> GetNew()
    101.             {
    102.                 var queue = new NativeQueue<T>(Allocator.TempJob);
    103.                 this.queues.Add(queue);
    104.                 return queue;
    105.             }
    106.  
    107.             /// <inheritdoc />
    108.             public void Update(EntityManager entityManager)
    109.             {
    110.                 if (this.entities.IsCreated)
    111.                 {
    112.                     entityManager.DestroyEntity(this.entities);
    113.                     this.entities.Dispose();
    114.                 }
    115.  
    116.                 var count = this.GetCount();
    117.  
    118.                 if (count == 0)
    119.                 {
    120.                     return;
    121.                 }
    122.  
    123.                 // Felt like Temp should be the allocator but gets disposed for some reason.
    124.                 this.entities = new NativeArray<Entity>(count, Allocator.TempJob);
    125.  
    126.                 entityManager.CreateEntity(this.archetype, this.entities);
    127.  
    128.                 int index = 0;
    129.  
    130.                 JobHandle handle = default;
    131.  
    132.                 var chunkIndex = new NativeUnit<int>(Allocator.TempJob);
    133.                 var entityIndex = new NativeUnit<int>(Allocator.TempJob);
    134.  
    135.                 var componentType = entityManager.GetArchetypeChunkComponentType<T>(false);
    136.  
    137.                 var chunks = entityManager.CreateArchetypeChunkArray(this.query, Allocator.TempJob);
    138.  
    139.                 foreach (var queue in this.queues)
    140.                 {
    141.                     handle = new SetJob
    142.                         {
    143.                             Chunks = chunks,
    144.                             Queue = queue,
    145.                             ChunkIndex = chunkIndex,
    146.                             EntityIndex = entityIndex,
    147.                             ComponentType = componentType,
    148.                         }
    149.                         .Schedule(handle);
    150.                 }
    151.  
    152.                 handle.Complete();
    153.  
    154.                 chunks.Dispose();
    155.                 chunkIndex.Dispose();
    156.                 entityIndex.Dispose();
    157.  
    158.                 foreach (var queue in this.queues)
    159.                 {
    160.                     queue.Dispose();
    161.                 }
    162.  
    163.                 this.queues.Clear();
    164.             }
    165.  
    166.             public void Dispose()
    167.             {
    168.                 if (this.entities.IsCreated)
    169.                 {
    170.                     this.entities.Dispose();
    171.                 }
    172.             }
    173.  
    174.             [BurstCompile]
    175.             private struct SetJob : IJob
    176.             {
    177.                 public NativeQueue<T> Queue;
    178.  
    179.                 public NativeArray<ArchetypeChunk> Chunks;
    180.  
    181.                 public NativeUnit<int> ChunkIndex;
    182.  
    183.                 public NativeUnit<int> EntityIndex;
    184.  
    185.                 public ArchetypeChunkComponentType<T> ComponentType;
    186.  
    187.                 /// <inheritdoc />
    188.                 public void Execute()
    189.                 {
    190.                     for (; this.ChunkIndex.Value < this.Chunks.Length; this.ChunkIndex.Value++)
    191.                     {
    192.                         var chunk = this.Chunks[this.ChunkIndex.Value];
    193.  
    194.                         var components = chunk.GetNativeArray(this.ComponentType);
    195.  
    196.                         var intLocalIndex = this.EntityIndex.Value;
    197.  
    198.                         while (this.Queue.TryDequeue(out var item) && intLocalIndex < components.Length)
    199.                         {
    200.                             components[intLocalIndex++] = item;
    201.                         }
    202.  
    203.                         this.EntityIndex.Value = intLocalIndex < components.Length ? intLocalIndex : 0;
    204.                     }
    205.                 }
    206.             }
    207.         }
    208.     }
    209. }
    210.  
    Requires my native unit container

    Code (CSharp):
    1. // <copyright file="NativeUnit.cs" company="Timothy Raines">
    2. //     Copyright (c) Timothy Raines. All rights reserved.
    3. // </copyright>
    4.  
    5. namespace BovineLabs.Common.Native
    6. {
    7.     using System;
    8.  
    9.     using Unity.Burst;
    10.     using Unity.Collections;
    11.     using Unity.Collections.LowLevel.Unsafe;
    12.  
    13.     /// <summary>
    14.     /// A single value native container to allow values to be passed between jobs.
    15.     /// </summary>
    16.     /// <typeparam name="T">The type of the <see cref="NativeUnit{T}"/>.</typeparam>
    17.     [NativeContainerSupportsDeallocateOnJobCompletion]
    18.     [NativeContainer]
    19.     public unsafe struct NativeUnit<T> : IDisposable
    20.         where T : struct
    21.     {
    22.         [NativeDisableUnsafePtrRestriction]
    23.         private void* m_Buffer;
    24.  
    25.         private Allocator m_AllocatorLabel;
    26.  
    27. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    28.         private AtomicSafetyHandle m_Safety;
    29.  
    30.         [NativeSetClassTypeToNullOnSchedule]
    31.         private DisposeSentinel m_DisposeSentinel;
    32. #endif
    33.  
    34.         /// <summary>
    35.         /// Initializes a new instance of the <see cref="NativeUnit{T}"/> struct.
    36.         /// </summary>
    37.         /// <param name="allocator">The <see cref="Allocator"/> of the <see cref="NativeUnit{T}"/>.</param>
    38.         /// <param name="options">The default memory state.</param>
    39.         public NativeUnit(Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory)
    40.         {
    41.             if (allocator <= Allocator.None)
    42.             {
    43.                 throw new ArgumentException("Allocator must be Temp, TempJob or Persistent", nameof(allocator));
    44.             }
    45.  
    46.             IsBlittableAndThrow();
    47.  
    48.             var size = UnsafeUtility.SizeOf<T>();
    49.             this.m_Buffer = UnsafeUtility.Malloc(size, UnsafeUtility.AlignOf<T>(), allocator);
    50.             this.m_AllocatorLabel = allocator;
    51.  
    52. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    53.             DisposeSentinel.Create(out this.m_Safety, out this.m_DisposeSentinel, 1, allocator);
    54. #endif
    55.  
    56.             if ((options & NativeArrayOptions.ClearMemory) == NativeArrayOptions.ClearMemory)
    57.             {
    58.                 UnsafeUtility.MemClear(this.m_Buffer, UnsafeUtility.SizeOf<T>());
    59.             }
    60.         }
    61.  
    62.         /// <summary>
    63.         /// Gets or sets the value of the unit.
    64.         /// </summary>
    65.         public T Value
    66.         {
    67.             get
    68.             {
    69. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    70.                 AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
    71. #endif
    72.                 return UnsafeUtility.ReadArrayElement<T>(this.m_Buffer, 0);
    73.             }
    74.  
    75.             [WriteAccessRequired]
    76.             set
    77.             {
    78. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    79.                 AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
    80. #endif
    81.                 UnsafeUtility.WriteArrayElement(this.m_Buffer, 0, value);
    82.             }
    83.         }
    84.  
    85.         /// <summary>
    86.         /// Gets a value indicating whether the <see cref="NativeUnit{T}"/> has been initialized.
    87.         /// </summary>
    88.         public bool IsCreated => (IntPtr)this.m_Buffer != IntPtr.Zero;
    89.  
    90.         /// <inheritdoc/>
    91.         [WriteAccessRequired]
    92.         public void Dispose()
    93.         {
    94.             if (!UnsafeUtility.IsValidAllocator(this.m_AllocatorLabel))
    95.             {
    96.                 throw new InvalidOperationException(
    97.                     "The NativeArray can not be Disposed because it was not allocated with a valid allocator.");
    98.             }
    99.  
    100. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    101.             DisposeSentinel.Dispose(ref this.m_Safety, ref this.m_DisposeSentinel);
    102. #endif
    103.             UnsafeUtility.Free(this.m_Buffer, this.m_AllocatorLabel);
    104.             this.m_Buffer = null;
    105.         }
    106.  
    107.         [BurstDiscard]
    108.         private static void IsBlittableAndThrow()
    109.         {
    110.             if (!UnsafeUtility.IsBlittable<T>())
    111.             {
    112.                 throw new ArgumentException($"{typeof(T)} used in NativeArray<{typeof(T)}> must be blittable");
    113.             }
    114.         }
    115.     }
    116. }
    Not really that tested, was really just testing theoretical performance.

    -edit-

    for some reason my SetJob doesn't burst compile at runtime, but burst compiles in editor. It's 3x faster when bursted (should save 0.5-1ms on these tests) but not sure what is causing it.
     
    Last edited: Dec 6, 2018 at 4:37 AM
    davenirline likes this.