Search Unity

  1. Unity 2019.1 is now released.
    Dismiss Notice

Batch EntityCommandBuffer

Discussion in 'Data Oriented Technology Stack' started by tertle, Dec 6, 2018.

  1. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    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
    Arkade, Creepgin, Kender and 12 others like this.
  2. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    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.
     
    hippocoder likes this.
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    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
    wobes likes this.
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Entities will live 1 frame exactly. Destroying entity is now done automatically.

    -edit 2018/12/28-

    did some unit testing, found some issues and fixed these. also added parallel setting support, should be quite a bit faster.

    Source code is now here: https://github.com/tertle/EventBatchSystem

    -edit 2019/01/09- Read below, repo moved.
     
    Last edited: Jan 9, 2019
    pahe, hippocoder and davenirline like this.
  5. Jay-Pavlina

    Jay-Pavlina

    Joined:
    Feb 19, 2012
    Posts:
    194
    @tertle Can you briefly explain how this EventSystem works? I am converting a game from Rx-driven events to ECS and have the most trouble with events.
     
  6. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Sure. So I have a few short lived entities with a single component attached that I use as kind of an event.

    For example to request a path in my project, you create a new entity with a FindPath component. This component will be handled by the pathfinding system (though other events might be handled by multiple systems) and then destroyed.

    The point of this, I think I called it BatchSystem for the moment as it shared the same name as the unity UI class though it really needs a better name, is to batch create all entity events at once (of the same type, though as I typed this I just realized I could maybe merge the creation of all of event entities at once) instead of creating them individually 1 at a time.

    So instead of using EntityCommandBuffer and creating the entity yourself, you BatchSystem.GetEventBatch<T>() where T where the event component. This will return a NativeQueue<T> which every time the method is called so you don't need to worry about other system dependencies and will be automatically merged. You simply add the event struct to this queue instead and when BatchSystem updates, it'll create a bunch of entities and set the component data.

    All these newly created entities will be stored in the BatchSystem and destroyed 1 frame later in a single batch destroy entity call. You don't need to worry about maintaining the life of the event entities it's handled for you. (Whichcan be very useful if your event entities are used by more than 1 system, you don't have to worry about ordering.)

    NativeQueue has a ToConcurrent() method so can be used in parallel but can also be burst compiled unlike entity command buffer.

    -edit-

    side note, if anyone was using this before today I updated the code with a small bug fix a few hours ago.
     
  7. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    Amazing work, thank you
     
  8. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    Can we have permission to use your BatchSystem and NativeUnit code?
    I'm asking because I've noticed the all rights reserved copyright.
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Oh yeah of course. They are just auto generated by StyleCop and I'm too lazy to strip it when copy/pasting.

    ~feel free to use any code I post on unity forums, now or in the future.

    -edit-

    side note, NativeUnit is really just a NativeArray with a fixed length of 1. More of a convenience.
     
    Last edited: Dec 19, 2018
    iam2bam likes this.
  10. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    Cool, thanks!

    I thought of it as a "NativeReference" :). Even if it's just like a 1-array, it makes code clearer and I'm all about that!

    I just hope someone figures out a way to return a reference, to access direct memory to get rid of the
    o = arr[0]; o.var = 123; arr[0] = o
    idiom for structs.
     
    Arkade likes this.
  11. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    finally got around to doing some unit testing and found some issues (and fixed them).
    Also added parallel setting support, should be quite a bit faster. -note- while this is passing all tests, i probably need to do some large scale validation to ensure thread safety.

    Source code is now here: https://github.com/tertle/com.bovinelabs.entities
     
    Last edited: Dec 30, 2018
    Antypodish and iam2bam like this.
  12. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,185
    A little recommendation about repo's "shape" to support UPM git pulling, if you move the git init location one level deeper inside the Assets folder then put package.json, then anyone can pull that from Unity. It would appears in Project window like other Unity packages. (Wrapped one level in a folder named after the name specified in package.json) dependencies will go in package.json instead of manifest.json. .gitignore will also has to be Assets in this case.

    I have 1 Unity project named PackageDev where in the Assets I have multiple folders representing each small package with its own repo. I set a local UPM on each folder so that the change applies to all games, and git would be able to take them online.
     
    Arkade and tertle like this.
  13. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Actually I forgot about this feature. I looked at it when it first came out but ran into an issue and totally forgot.

    I've moved my code to here: https://github.com/tertle/com.bovinelabs.entities if anyone wants.
     
    Arkade and wobes like this.
  14. wobes

    wobes

    Joined:
    Mar 9, 2013
    Posts:
    641
    @tertle could you add a small ReadMe that describes how to get started if it's possible?
     
  15. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Yeah sure, I'm about to head out for NYE so I'll do it soon as I get back in the new year.
     
    wobes likes this.
  16. wobes

    wobes

    Joined:
    Mar 9, 2013
    Posts:
    641
    Happy holidays to you.
     
    Antypodish likes this.
  17. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    wobes likes this.
  18. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    125
    Awesome work! Is there a way to add multiple IComponentData to an entity? Also, is it possible to add an ISharedComponentData?
     
  19. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    4,580
    Looks like cool work has been done here.
    But got question, since I maybe missed, or misunderstood.
    You have mentioned short leaving entities. But is this approach good for creating persistent entities? Sorry, haven't chance yet, to test the code.
     
  20. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Neither of these are that hard to add but I don't really see the point of them for this particular sytem.

    Yes, if you're creating a lot of (persistent) entities in a frame you should batch create them, much faster (this system won't do that for you though.)
     
    Last edited: Jan 8, 2019
    Antypodish likes this.
  21. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Found kind of an elegant solution my dependency issues that someone might be interested in.

    So previously to handle dependencies, I was grabbing the internal JobHandle m_PreviousFrameDependency with reflection but unfortunately as JobHandle is a struct, and without reflection and GetValue is designed structs are always boxed it was generating garbage each frame.

    I've gone in and redesigned how I handle dependencies now. Instead, I pass a fake BarrierSystem to the JobComponentSystem (handled automatically with reflection once per life of JobComponentSystem) and then manually update this system.

    It works remarkably well. The barrier doesn't have any command buffers, but just completes all dependencies. This system is lazy created to avoid being added to the default loop, but if you happen to use this and update your loop mid way through for some reason, this will break.

    TLDR: nothing has changed in how you use it, just no more garbage.
     
  22. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    264
    So I read all of this code and like it very much. I have one question regarding this section:
    Code (CSharp):
    1.   private class EventBatch<T> : EventBatchBase
    2.             where T : struct, IComponentData
    3.         {
    4.             private readonly List<NativeQueue<T>> queues = new List<NativeQueue<T>>();
    5.  
    6. // .. SNIP ...
    7.  
    8.             public NativeQueue<T> GetNew()
    9.             {
    10.                 // Having allocation leak warnings when using TempJob
    11.                 var queue = new NativeQueue<T>(Allocator.Persistent);
    12.                 this.queues.Add(queue);
    13.  
    14.                 return queue;
    15.             }
    16. }
    Why is it necessary to create multiple queues per event type?
    NativeQueue 
    has a
    ToConcurrent
    method which would make the Queue safe to use from multiple threads and we actually don't really care about the order of things as all entities get created in one fell swoop. So just from looking at it I would say you could do with a single queue (which would simplify some code later where it maps the single queues to segments in the chunk). Then again, I have little experience with this, so I would really like to know why you chose to design it that way. Thanks for sharing this!
     
  23. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Because this

    Code (CSharp):
    1.         var handle1 = new Job1
    2.             {
    3.                 Entities = this.queue.ToConcurrent()
    4.             }
    5.             .Schedule(this, inputDeps);
    6.  
    7.         var handle2 = new Job2
    8.             {
    9.                 Entities = this.queue.ToConcurrent()
    10.             }
    11.             .Schedule(this, inputDeps);
    12.  
    13.         var handle = JobHandle.CombineDependencies(handle1, handle2);
    Will throw this specific error

    Note job1 dependency isn't passed to job2 so these jobs run in parallel, not sequential (which is fine.)

    NativeContainers.Concurrent are only safety checked within the same job, not across multiple jobs so by creating a separate container for each system I avoid these dependency issues.
     
    Last edited: Jan 25, 2019
    kork likes this.
  24. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    264
    So is this really a problem or is it just a false positive of the safety system? My thinking is that if a
    NativeContainer.Concurrent
    is safe to use by multiple threads within one job, in theory it should also be safe to use across jobs, after all in both cases you have multiple threads concurrently accessing the container.

    So if I would add some
    NativeDisableContainerSafetyRestriction 
    attribute to
    Entities 
    and therefore circumvent the suspected false positive, would it work? I'm not saying that this is a particulary good idea, I just would like to know if I understood the thing correctly :D
     
  25. M_R

    M_R

    Joined:
    Apr 15, 2015
    Posts:
    423
    1) different jobs scheduling ordering is non-deterministic without dependencies, even on the same thread. (this is also true for different "parts" of the same parallel job, but a parallel job should be designed to do the same thing to all of its parts, so it shouldn't matter
    2) concurrent containers may work by using an internal [ThreadIndex] and that may be set uniquely only in a single .Schedule call (e.g. different scheduled jobs may have conflicting threadIndex in different threads, we can't know unless @Joachim_Ante tells us otherwise)
     
  26. tbriley

    tbriley

    Joined:
    Sep 10, 2013
    Posts:
    107
    is it possible to use this with UPM? am getting a package dependency error when adding "com.bovinelabs.entities": "0.1.2" to my manifest (2019.1.0b5). am a bit new with how custom packages work.
     
  27. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,251
    Been busy with something so haven't properly updated my libraries. I have updated this to the latest version of entities but I haven't updated the dependencies of the package yet. I'll l do that tomorrow thanks for letting me know.
     
  28. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,185
    This is how to UPM to GitHub
     "com.bovinelabs.entities": "git://github.com/tertle/com.bovinelabs.entities.git",
    to update delete the new lock section appearing in your manifest after the first pull.
     
    Anichale, kork and tbriley like this.