Search Unity

Flagging for filtering: Better to add/remove empty component or poll over component data int.

Discussion in 'Entity Component System' started by iam2bam, Dec 16, 2018.

  1. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    Hello again, I've got an architectural question of sorts

    With some experience with ECS outside Unity, I've seen this pattern: add component to flag an entity so some system runs on it. Sometimes they're just a bitmask (no data). But I've seen them probably on a different memory structuring version of ECS.

    I gather from the tutorials about the way I think chunking might work, changing structure of several entites might start some memory reorganization process that's eventually slower than just polling through hundreds of thousands and and-ing to check some flag against a small integer from component data (Even though I guess some small penalty from branch mispredictions might arise as there is no way to define sort order on them)

    I've tried it, deferring additions and removals with a barrier concurrent-able buffer, I get framerate spikes on the given barriers (while running the commands, not on the jobs adding them).

    Have any of you profiled this? Is there a turning point by amount for when it's better to add-component or set-data-flag?
    Is there another way to handle this that I'm not aware of? When I have more time I'll do the testing myself but maybe there's a better way to handle this and I can avoid investing my time on that.

    Use case: Only sync/mirror data to classic game objects when really necessary (only when changed)

    Thanks!
     
    Last edited: Dec 17, 2018
  2. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    I've done some tests of a similar issue and found that creating an event as a separate entity is similar in performance to iterating over persistent entities where 0.1% of queried entities have that event. And if that even is true in 1+% of cases, then it is definitely more performant to iterate through all entities and check than to have it as a separate new event entity. But take it with a grain of salt, my benchmark was a bit messy.

    Haven't tested adding component vs checking a value in a component. Would like to see the results or test it at some point.

    And yes, the barrier's entityCommandBuffer will playback on the main thread so you'll get the spikes.
     
    Last edited: Dec 17, 2018
  3. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    My janky benchmark in pictures:
    upload_2018-12-17_14-28-30.png

    upload_2018-12-17_14-41-48.png

    1) Setting and checking a 1000 components
    2) Adding a 1000 components
    3) Adding 10 components
    4) Adding 100 components

    I don't think there needs to be a bigger scale benchmark since it becomes nonviable for me at less than 1000 components per second. Interestingly a 1000 and a 100 are almost identical.

    A bigger benchmark is needed for cases where less than 1% of entities get flagged.

    So, a tag component filters might be good for some very occasional events, but in other cases, it is better to have a persistent component and to poll over them.
     
    Last edited: Dec 18, 2018
    deus0, NotaNaN, iam2bam and 1 other person like this.
  4. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    upload_2018-12-17_20-12-0.png

    1) Adding and removing 1000 entities
    2) Iterating over 1,000,000 entities

    upload_2018-12-17_20-20-0.png

    1) Adding and removing 1000 entities
    2) Iterating over 10,000,000 entities


    upload_2018-12-17_20-18-15.png

    1) Spawining 100,000,000 entities

    Code (CSharp):
    1. struct TestComponent : IComponentData { public int Active; }
    2. struct TestComponent2 : IComponentData { }
    3. struct TestComponent3 : IComponentData { }
    4.  
    5. public class IterateSystem : JobComponentSystem
    6. {
    7.     override protected void OnCreateManager()
    8.     {
    9.         for (int i = 0; i < 10000000; i++)
    10.         {
    11.             EntityManager.CreateEntity(ComponentType.Create<TestComponent>());
    12.         }
    13.     }
    14.  
    15.     [BurstCompile]
    16.     struct Job : IJobProcessComponentData<TestComponent>
    17.     {
    18.         public void Execute(ref TestComponent c)
    19.         {
    20.             if (c.Active != 0)
    21.             {
    22.             }
    23.         }
    24.     }
    25.  
    26.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    27.     {
    28.         var job = new Job { };
    29.         return job.Schedule(this, inputDeps);
    30.     }
    31. }
    32.  
    33. public class AddComponentSystem : JobComponentSystem
    34. {
    35.     [UpdateAfterAttribute(typeof(AddComponentSystem))]
    36.     class Barrier : BarrierSystem { }
    37.     [Inject] Barrier b;
    38.     [Inject] EndFrameBarrier efb;
    39.  
    40.     override protected void OnCreateManager()
    41.     {
    42.         for (int i = 0; i < 1000; i++)
    43.         {
    44.             EntityManager.CreateEntity(ComponentType.Create<TestComponent2>());
    45.         }
    46.     }
    47.  
    48.     struct Job : IJobProcessComponentDataWithEntity<TestComponent2>
    49.     {
    50.         public EntityCommandBuffer.Concurrent ecb;
    51.         public EntityCommandBuffer.Concurrent eecb;
    52.         public void Execute(Entity e, int i, ref TestComponent2 c)
    53.         {
    54.             ecb.AddComponent<TestComponent3>(i, e, new TestComponent3());
    55.             eecb.RemoveComponent<TestComponent3>(i, e);
    56.         }
    57.     }
    58.  
    59.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    60.     {
    61.         var job = new Job { ecb = b.CreateCommandBuffer().ToConcurrent(), eecb = efb.CreateCommandBuffer().ToConcurrent() };
    62.         return job.Schedule(this, inputDeps);
    63.     }
    64. }
     
    Last edited: Dec 17, 2018
  5. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    This basically means that on 4 core CPU currently if I have an object that is receiving an event less often than every 10,000 - 20,000 frames, it is probably more efficient to iterate than to flag with a component.

    Removing takes twice as long as adding a tag component. So if you change state only one way the numbers are different.

    For me, it means that most of the things in my game will have persistent components because the events on them generally happen more often than those 100 - 200 seconds. This also makes performance less spiky.
     
    Last edited: Dec 17, 2018
    sirwhatevers, Spy-Shifty and iam2bam like this.
  6. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    It was brought to my attention that creating separate entities might be faster than tagging existing.

    upload_2018-12-17_21-29-30.png

    1) Well, that's creating and destroying a 1000 entities. Can't find batching API. Using archetype.

    Also, I checked the difference between tagging an entity with one or 7 components already on it. No difference found. EDIT: There actually is a slight difference between tagging an entity with less or more data.
     
    Last edited: Dec 17, 2018
  7. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    These might be the most amazing replies I've ever got. Thanks for your time and your info!
    Also thanks for profiling adding/removing too.

    I haven't thought of entities with just the one component, too bad they don't work either. I'd probably need more than 1% so I'll go for polling. Maybe I'll just need to have a persistent NativeArray, too bad I can't write trivially in parallel.

    Maybe the overhead comes from chunk allocation, even for a single-component archetype?
    If I understand correctly, chunk size / int32 component index + entity id = 16kB / 8 = 2048.
    That could explain having similar performance up to two thousand entities.
    Also found:
    Code (CSharp):
    1.         public const int kChunkSize = 16 * 1024 - 256; // allocate a bit less to allow for header overhead
    2.         public const int kMaximumEntitiesPerChunk = kChunkSize / 8;
    Reading this I found why it's a terrible idea to add/remove, because it changes it's archetype and moves all component data to a different chunk!
    https://forum.unity.com/threads/ecs-memory-layout.532028/#post-3513888

    :D
     
  8. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    No problem, I needed this info myself :)

    That was also a very dirty benchmark, actually my game code, a bunch of overhead.

    Yes. Also, I'm gonna add an edit. There actually is a slight difference between tagging an entity with less or more data.
     
    iam2bam likes this.
  9. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    By the way.. In the light of all the above, Entity pooling makes sense when we need to create a lot of entities each frame or save some performance in general.

    Also, I missed something in the benchmark, the system that iterates over millions of entities should also SetComponent<>() for a 1000 entities to be fair.
     
  10. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    True! Also try add/rem of SharedComponentData as tag, because it's saved once per chunk. For tagging it would probably just keep only two sets of chunks and move between them, maybe?
    I'll get on it when I get back.
     
  11. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    The important update:

    upload_2018-12-18_7-30-42.png

    1) Setting a 1000 components 2 times and Iterating over 10,000,000 components
    2) Tagging entities with data by adding and removing a 1000 tag entities

    This is the more fair comparison between tagging and polling.

    So even when events occur very rarely 1/10000 the largest cost comes from setting a component, not from polling components. If they occur 1/100 the polling cost is negligible, but setting a component is a cost to keep in mind although it is significantly lower than the tagging cost.


    I don't think tagging with an ISharedComponentData is viable. It still gonna move each entity individually to a new chunk.
     
    Last edited: Dec 18, 2018
    Rouddem and iam2bam like this.
  12. Jay-Pavlina

    Jay-Pavlina

    Joined:
    Feb 19, 2012
    Posts:
    195
    I was wondering about this too. Thanks for the analysis. Looks like setting and polling is the way to go.
     
    illinar likes this.
  13. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    You can greatly improve performance of creating and destroying entities by doing them in a batch instead of creating them 1 by 1.

    EntityManager.CreateEntity(EntityArchetype, NativeArray<Entity>)


    Can see my experimentation here (and my final solution in the last comment that batches entity event creation): https://forum.unity.com/threads/batch-entitycommandbuffer.593569/

    Result Summary:

    I got more than a 10x speedup over using EntityCommandBuffer CreateEntity/SetComponentData.
    Creating, set component data (and destroying previous frame) 100,000 entities in 3.4ms (~300fps)
     
    Last edited: Dec 18, 2018
  14. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Wow... Pretty amazing results. Do I even need to tell which is which? They are almost identical.

    upload_2018-12-18_12-40-35.png

    Blue - Setting a 1000 components twice on the main, and iterate over a 10,000,000 in a job.
    Red - Creating and destroying a 1000 event entities on the main, and use them via ComponentDataFromEntity in a job. Number of existing target entities doesn't matter, so imagine it's also 10,000,000

    This makes a separate entity based event system very much viable, which is great since it might be a better design. Except the whole create the entity, connect to the target and then get data from its target process requires a lot of extra steps and buffering when you want to use batching. If API gets better at it, this could be a great way though.

    Also, need to note that it is only that fast for the entities that are living only one frame because they are deleted by archetype all at once.

    EDIT: I forgot burst while using the event entities, and with burst, it almost disappears from the worker thread, so the batched event entities are actually faster in the benchmark.

    upload_2018-12-18_13-14-56.png

    The code.
    Code (CSharp):
    1. using Unity.Entities;
    2. using Unity.Burst;
    3. using Unity.Jobs;
    4. using Unity.Collections;
    5. using Unity.Transforms;
    6. using Unity.Mathematics;
    7. using UnityEngine;
    8.  
    9. struct TestIterationComponent : IComponentData { public int Value; }
    10. struct TestEventComponent : IComponentData { public Entity Target; }
    11. struct TestTargetComponent : IComponentData { public int i; }
    12.  
    13. [UpdateBeforeAttribute(typeof(CreateEntitySystem))]
    14. public class IterateSystem : JobComponentSystem
    15. {
    16.     struct Targets
    17.     {
    18.         public ComponentDataArray<TestIterationComponent> Component;
    19.         public EntityArray Entities;
    20.     }
    21.     [Inject] Targets components;
    22.  
    23.     override protected void OnCreateManager()
    24.     {
    25.         for (int i = 0; i < 1000000; i++)
    26.         {
    27.             EntityManager.CreateEntity(ComponentType.Create<TestIterationComponent>());
    28.         }
    29.     }
    30.  
    31.     [BurstCompile]
    32.     struct Job : IJobProcessComponentData<TestIterationComponent>
    33.     {
    34.         public void Execute(ref TestIterationComponent c)
    35.         {
    36.             if (c.Value != 0)
    37.             {
    38.             }
    39.         }
    40.     }
    41.  
    42.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    43.     {
    44.         for (int i = 0; i < 2000; i++)
    45.         {
    46.             EntityManager.SetComponentData<TestIterationComponent>(components.Entities[i], new TestIterationComponent { Value = 1 });
    47.         }
    48.         var job = new Job { };
    49.         return job.Schedule(this, inputDeps);
    50.     }
    51. }
    52.  
    53. public class CreateEntitySystem : ComponentSystem
    54. {
    55.     EntityArchetype eventArvhetype;
    56.     struct Targets
    57.     {
    58.         public ComponentDataArray<TestTargetComponent> T;
    59.         public EntityArray Entities;
    60.     }
    61.     [Inject] Targets targets;
    62.  
    63.     override protected void OnCreateManager()
    64.     {
    65.         eventArvhetype = EntityManager.CreateArchetype(typeof(TestEventComponent));
    66.  
    67.         var newTargets = new NativeArray<Entity>(1000000, Allocator.Temp);
    68.         EntityManager.CreateEntity(EntityManager.CreateArchetype(typeof(TestTargetComponent)), newTargets);
    69.         for (int i = 0; i < 1000000; i++)
    70.         {
    71.             EntityManager.SetComponentData<TestTargetComponent>(newTargets[i], new TestTargetComponent { i = 1 });
    72.         }
    73.         newTargets.Dispose();
    74.     }
    75.  
    76.     protected override void OnUpdate()
    77.     {
    78.         var targetEntities = new NativeArray<Entity>(1000, Allocator.Temp);
    79.         for (int i = 0; i < 1000; i++)
    80.         {
    81.             targetEntities[i] = targets.Entities[i];
    82.         }
    83.         var events = new NativeArray<Entity>(1000, Allocator.Temp);
    84.         EntityManager.CreateEntity(eventArvhetype, events);
    85.         for (int i = 0; i < 1000; i++)
    86.         {
    87.             EntityManager.SetComponentData<TestEventComponent>(events[i], new TestEventComponent { Target = targetEntities[i] });
    88.         }
    89.         events.Dispose();
    90.         targetEntities.Dispose();
    91.     }
    92. }
    93.  
    94. [UpdateAfter(typeof(CreateEntitySystem))]
    95. public class UseEntitiesSystem : JobComponentSystem
    96. {
    97.     [Inject] ComponentDataFromEntity<TestTargetComponent> componentDataFromEntity;
    98.     ComponentGroup g;
    99.  
    100.     override protected void OnCreateManager()
    101.     {
    102.         g = EntityManager.CreateComponentGroup(typeof(TestEventComponent));
    103.     }
    104.  
    105.     [BurstCompile]
    106.     struct Job : IJobProcessComponentDataWithEntity<TestEventComponent>
    107.     {
    108.         [ReadOnly] public ComponentDataFromEntity<TestTargetComponent> componentDataFromEntity;
    109.  
    110.         public void Execute(Entity e, int i, ref TestEventComponent c)
    111.         {
    112.             var d = componentDataFromEntity[c.Target];
    113.             if (d.i == 0)
    114.             {
    115.                 d.i = 1;
    116.             }
    117.             else
    118.             {
    119.                 d.i = 0;
    120.             }
    121.         }
    122.     }
    123.  
    124.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    125.     {
    126.         var job = new Job
    127.         {
    128.             componentDataFromEntity = componentDataFromEntity
    129.         };
    130.         return job.Schedule(this, inputDeps);
    131.     }
    132. }
    133.  
    134. [UpdateAfter(typeof(UseEntitiesSystem))]
    135. public class DestroyEntitySystem : ComponentSystem
    136. {
    137.     ComponentGroup g;
    138.  
    139.     override protected void OnCreateManager()
    140.     {
    141.         g = EntityManager.CreateComponentGroup(typeof(TestEventComponent));
    142.     }
    143.  
    144.     protected override void OnUpdate()
    145.     {
    146.         EntityManager.DestroyEntity(g);
    147.     }
    148. }
    Now I'm gonna try to find a way to make event entities pretty and usable in the code. For state switching setting components is still better of course. For things like JustSpawned, IncomingDamage, GameOver, the event entities are the best. Especially Damage in this case since you can add as many instances as you wish.

    The difference is more in favor of event entities when there are more of them.
    - Setting 10,000 and iterating costs 9ms because we are setting twice
    - Create>Set>Destroy 1.5ms
    Actually doesnt make sense, I would expect 9 and 4.5 ms at least.
     
    Last edited: Dec 18, 2018
    NotaNaN and Spy-Shifty like this.
  15. Srokaaa

    Srokaaa

    Joined:
    Sep 18, 2018
    Posts:
    169
    Best thread ever! Thank you very much for benchmarks, very insightful. I was wondering about the SharedComponent as well but to use it in a slightly different manner. Usually whenever I need an event that some system will receive I create a separate Entity on that system. Like this:


    Code (CSharp):
    1.     public class SpawnEnemiesSystem : JobComponentSystem
    2.     {
    3.         public struct SpawnEnemiesEvent : IComponentData { }
    4.  
    5.         public           Entity          SpawnEnemiesEventEntity;
    6.         [Inject] private EndFrameBarrier _endFrameBarrier;
    7.  
    8.         protected override void OnCreateManager()
    9.         {
    10.             SpawnEnemiesEventEntity = EntityManager.CreateEntity(typeof(LevelDataComponent));
    11.         }
    12.  
    13.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    14.         {
    15.             return new SpawnEnemiesJob
    16.             {
    17.                 CommandBuffer = _endFrameBarrier.CreateCommandBuffer().ToConcurrent()
    18.             }.Schedule(this, inputDeps);
    19.         }
    20.  
    21.         private struct SpawnEnemiesJob : IJobProcessComponentDataWithEntity<SpawnEnemiesEvent, LevelDataComponent>
    22.         {
    23.             public EntityCommandBuffer.Concurrent CommandBuffer;
    24.  
    25.             public void Execute(Entity                 entity,
    26.                                 int                    index,
    27.                                 ref SpawnEnemiesEvent  eventComponent,
    28.                                 ref LevelDataComponent levelData)
    29.             {
    30.                 // DO WHATEVER YOU NEED TO ON THIS EVENT
    31.              
    32.                 //Remove event
    33.                 CommandBuffer.RemoveComponent<SpawnEnemiesEvent>(index, entity);
    34.             }
    35.         }
    36.     }
    37.  
    If I need to trigger this event I just [Inject] this system into other system, get an entity and add SpawnEnemiesEvent to it. As was mentioned above using SharedComponent causes every entity to use the same Chunk regardless of it's Archetype. So maybe using SharedComponent not as marker that event occurred but rather as a marker that entities which receive events shouldn't create new Archetype on adding a Component would work. Something like this:


    Code (CSharp):
    1.         public struct EventSharedComponent : ISharedComponentData { }
    2.  
    Now just set this SharedComponent to every entity that will have marker components added to it and and they shouldn't create new Archetype when the marker component is actually added.

    I'll try to create some benchmarks this week if I manage to find some time
     
  16. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Absolutley incorrect. SCD divide entities by chunks by SCD value inside archetype. For example you have A,B,C - IComponentData and D - ISharedComponentData.
    You have 3 archetypes:
    [A,D]
    [B,D]
    [C,D]
    in all cases, lets say, D has in field Value with value = 0
    And, lets say, 3 chunks inside every archetype:
    [A,D] - [C1, C2, C3]
    [B,D] - [C1, C2, C3]
    [C,D] - [C1, C2, C3]
    Then we change D Value to "1" for two entities inside C3 chunk of CD archetype, and we see:
    [A,D] - [C1, C2, C3]
    [B,D] - [C1, C2, C3]
    [C,D] - [C1, C2, C3, C4]
    Where in C4 we see thes two entities grouped by other SCD value.
     
    Last edited: Dec 18, 2018
  17. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    SharedComponentData is not forcing entities into the same chunks. If you have 10 chunks of entities and then you add Same shared component data to all entities and change value of a half of SCDs, you won't connect chunks into two chunks based on SCD values, you will fragment the chunks into 20 chunks. (given that each chunk has both SCD values)
     
  18. Srokaaa

    Srokaaa

    Joined:
    Sep 18, 2018
    Posts:
    169
    So if every entity has the same value of a SCD they all stay in the same chunk no matter what other components do they have added to them, correct? As I written above SCD would only be used to force the entites into the same chunk so they don't create a new one if we add a marker component to any of them. I might be wrong though
     
  19. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    No. They will be in chunks corresponding to their archetypes.
     
  20. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    As eizenhorn said, no. Every entity archetype would have its own chunk. They will only be in the same chunk if both the value of the scd and the archetype are the same.

    Scd causes significantly more chunks and should be avoided where possible.
     
    iam2bam likes this.
  21. Srokaaa

    Srokaaa

    Joined:
    Sep 18, 2018
    Posts:
    169
    All right, I had a misconception on how SCDs work then. Sorry for causing the confusion and thanks for explanation
     
  22. Jay-Pavlina

    Jay-Pavlina

    Joined:
    Feb 19, 2012
    Posts:
    195
    I thought we should not use entities for events because then it would create too many archetypes/chunks? Am I correct in this thinking or is there a workaround?
     
  23. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Thinking about the entity event approach so far I found some significant limitations. It doesn't allow polymorphism.

    At least take the following case. There are projectiles of different kinds and other objects. They have RaycastEveryFrame component and RaycastSystem does the raycasting. When a raycast hits the RaycastCollision event happens.

    If I have RaycastCollision as a component on projectile I can then have any number of different systems respond according to any given projectile entity archetype.

    Let's say one system deals with ReflectiveProjectile RaycastCollision another with HitScanProjectile, another with ExplosiveProjectile, another with TargetingSystem, VisionSensor, etc. Then all they are using one raycast system to do all this stuff that you can imagine they do.

    But if RaycastCollision is a separate entity, then I can't do that. If I want any meaningful response, the RaycastSystem has to know about what type of object is raycasting and add a specific raycast type like VisionSensorRaycastCollision, or two components, in any case, that's not an acceptable design.
     
    Last edited: Dec 18, 2018
    iam2bam likes this.
  24. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    In fact separate event entities create fewer chunks. One event type - one chunk type. But when you add different events to entities directly that creats a lot of archetypes because of all the different combinations of events.

    So considering my last post I think static components are the best approach most of the time. Just add all events that can happen to entity in advance, and this will also keep the archetype count way down, in comparison to when event components can be added and removed.
     
    Last edited: Dec 18, 2018
  25. Jay-Pavlina

    Jay-Pavlina

    Joined:
    Feb 19, 2012
    Posts:
    195
    So just have one archetype with a lot of different event components, and when there is an event, create the entity and then set the component for that event?
     
  26. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    No I mean approach one - separate entities with just one component which is the event type and a field for the entity which is the event target. Good sometimes, not good most of the time it seems.

    Another approach have all the events on the entity with a Bool flag. Like Collision.Happened = true/false. Iterate through all objects with Collision and react to those that have Happened = true.
     
    Last edited: Dec 18, 2018
  27. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    For example all my projectiles have RaycastCollision all the time even thought the event happens only once a few second or milliseconds. All damageable objects will probably have IncomingDamage all the times (but this actually might be bad design beacuse there are many sources of damage. while there is one source or RaycastCollision so there would be no conflict)
     
  28. Jay-Pavlina

    Jay-Pavlina

    Joined:
    Feb 19, 2012
    Posts:
    195
    I think we are saying similar things. When possible, the components for events should already exist, and when the event happens you set event data on the component and you look for that change in systems. This way you avoid a lot of archetypes.
     
    iam2bam and illinar like this.
  29. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Yes, I see what you were saying now. I think there are some rules we can outline. An event with one source and multiple recipients should probably be a constant tag component. An event with many sources and one recipient should be an entity event (e.g. Attach request for TransformSystem).

    Not sure how true that is.
     
    Last edited: Dec 18, 2018
  30. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    illinar and tertle, you're my heroes!

    One thing regarding the tests:
    Are you sure Burst or even plain C# doesn't optimize out the empty if statement?

    So to recap,

    1. Single event-component entities, one per event, work the fastest for arbitrary events but lack "archetype oriented" polymorphism.
    Data must have event-type (Explicit in component type) and event-source reference to respond different. Not very ECSy, but could be sorted then processed in batches.
    1 archetype (CD), 1 chunk type.
    (What about avoiding ECS for this feature, and just use a NativeQueue.Concurrent as tertle's work shows?)

    2. To keep "polymorphism", the approach is always keep a customized "event data" ComponentData around (even if no event happened), and another SharedComponentData with only a boolean-ish flag when it actually happens and have them all separate in a pair of chunk-types by assigned shared value (could even avoid CPU branching mispredictons).
    Process everything, check if happened from shared component bool, but gets exact event data from each ComponentData to respond.
    1 archetype (CD+SCD), 2 chunk types.
    EDIT: Could use ComponentGroup.SetFilter to avoid iterating chunks with not-event-set shared data.

    3. Or add/remove an "empty" (always same value) SharedComponentData as tag?
    Normal system filtering (very ECSy), gets exact event data from each ComponentData.
    2 archetypes (CD only, CD+SCD), 2 chunk types.
    EDIT: SharedComponentData can't be removed :eek:
    EDIT: You can remove SCDs, I was sleepy and got confused with asymetric semantics of the API (No RemoveSharedComponent)
     
    Last edited: Dec 19, 2018
    illinar likes this.
  31. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Good summary. But I'm not sure why you would use shared component data
    Just have the event on the entity and inside it a way to identify if it was triggered.

    The type of the event is the ComponentType, so it is a classic ECS, there is no reason to make event type a field, as far as I can tell. Works really well when there is just one system consuming that event, or each system that is consuming has all the relevant information without getting bloated. Attach - perfect case.

    Now thinking about it, you can have some polymorphism with event entities, but it costs. The cost is ComponentDataFromEntity access. Say you have damage system. Damage system receives all Damage entities, takes their
    public Entity Target;
    values and then uses all necessary injected ComponentDataFromEntity<> like
    ComponentDataFromEntity<Health>,
    ComponentDataFromEntity<Armor>,
    ComponentDataFromEntity<Immune>,
    ComponentDataFromEntity<AmplifyIncomingDamage>
    , etc.
    That access can be Burst compiled. This needs to be tested, because it is actually pretty fast, despite no cache coherency. This way you can have multiple incoming damage instances per entity as well.


    Are you sure Burst or even plain C# doesn't optimize out the empty if statement?
    I think It didn't. At least the jobs were always executing, and basically that what mattered the most, even if they were empty. One case where it did matter is ComponentDataFromEntity access, which actually might have been optimized out by Burst. I'm gonna test multiple ComponentDataFromEntity + event entity approach.
     
    Last edited: Dec 19, 2018
  32. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    SetFilter uses ComponentGroupFilter and SCD index for setting m_Filter which used in ComponentChunkIterator, and next it's does chunk iteration. Direct chunk iteration with filtering by SCD index more performant than using SetFilter which has some additional type checks and index findings. Besides with direct chunk iteration you can filter inside job, which mean it can be done much faster and parallel.
    No true, you can easy remove SCD like any other component.
     
    Last edited: Dec 19, 2018
  33. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    I got some new interesting results. Event entities might be much better than I thought.

    upload_2018-12-19_9-56-47.png

    Red - Set & Iterate (10,000 events on 1,000,000 targets)
    Blue - Event Entities (10,000 events on 1,000,000 targets)

    What's different this time is that I'm using Checking ComponentDataFromEntity.Exists() and setting it.

    upload_2018-12-19_10-0-20.png

    Every line is reached and executed, nothing is optimized out.

    To check 5 components and to write into them is a pretty decent number. If you increase this x2 then the job will take about x1.75 longer. Unfortunately, ComponentDataFromEntity can't be written into in parallel so I'm using a regular job there.

    The point is that ComponentDataFromEntity is cheap enough for event entities to be more performant in general in most cases. And you can have polymorphism with additional checks. Many systems can receive the event, and do the checks and write, especially if you make Event.Consumed Bool because that way the following systems won't need to do the expensive ComponentDataFromEntity checks and will ignore the event.

    This one is a bit harder to parallelize than Set&Iterate approach, so watch out for that.

    I'm changing my mind a lot during this thread. I'm still gonna use polling for the collisions because their use is so ubiquitous but probably event entities for damage since fewer systems will process damage and I can have multiple instances per target.



    Polling is good for events targeting many archetype specific consumer systems but is less ECS-clean and is not efficient for very rare events. Very good for state switching.

    Event entities can be polymorphic too but are bulkier, harder to parallelize, and costs scale with the number of checks but still are very acceptable in most cases and are deferred to a worker thread. Can only be better for one-frame events.
     
    Last edited: Dec 19, 2018
    iam2bam likes this.
  34. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    If you mean "can't" instead "can" (because you say Unfortunately before) then you wrong :) You can use ComponentDataFromEntity in parallel job.
     
  35. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Yeah, I meant can't, it was telling me that the container doesn't support it, and doesn't allow me to declare non-ReadOnly CDFE. Do I need to disable safety with an attribute or sth?
     
  36. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    [NativeDisableParallelForRestriction]

    Just make sure what you're doing is safe. In general if you're just using the entity passed in it'll be fine. Issues arise if you reference other entities.
     
    iam2bam likes this.
  37. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    And again my Indicators System :)

    Code (CSharp):
    1.  [BurstCompile]
    2.     private struct
    3.         ProcessIndicatorsMoving : IJobProcessComponentDataWithEntity<IndicatorComponent, WorldMeshRenderBounds>
    4.     {
    5.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<Position>        posArray;
    6.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<HasIndicator>    entities;
    7.         [ReadOnly]                            public BufferFromEntity<IndicatorBufferElement> buffer;
    8.         [ReadOnly]                            public int                                      clusterIndex;
    9.         [ReadOnly]                            public float                                    clusterCellSize;
    10.         public EntityCommandBuffer.Concurrent ecb;
    11.  
    12.         public void Execute(Entity indicatorEntity, int index, [ReadOnly] ref IndicatorComponent res,
    13.             ref WorldMeshRenderBounds bounds)
    14.         {
    15. ..
     
  38. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Guys..... I think I found a loophole, I actually can't believe what I'm seeing or the fact that no one have mentioned this option.

    upload_2018-12-19_12-11-8.png

    Blue - SetComponent&Poll
    Red - SetViaComponentDataFromEntity and poll

    This blows everything else out of the water. It takes no time on the main thread, it is fully parallel, it is easy. It actually works, I did the console outputs. You know how much faster this is than tagging or everything else?

    I actually made 10x less events by mistake on that picture, so I made 10000 events, and this is the difference that it made:
    upload_2018-12-19_12-19-45.png

    Screw that, here is a million events per frame at ~4000 fps (Edit: no it'snot 4k)
    upload_2018-12-19_12-23-25.png

    It actually works. I'm doing the output.

    upload_2018-12-19_12-26-53.png

    Code (CSharp):
    1. using Unity.Entities;
    2. using Unity.Burst;
    3. using Unity.Jobs;
    4. using Unity.Collections;
    5. using Unity.Transforms;
    6. using Unity.Mathematics;
    7. using UnityEngine;
    8.  
    9. // struct TestIterationComponent : IComponentData { public int Value; }
    10. // // [UpdateBeforeAttribute(typeof(CreateEntitySystem))]
    11. // public class SetComponentPollSystem : JobComponentSystem
    12. // {
    13. //     struct Targets
    14. //     {
    15. //         public ComponentDataArray<TestIterationComponent> Component;
    16. //         public EntityArray Entities;
    17. //     }
    18. //     [Inject] Targets components;
    19.  
    20. //     override protected void OnCreateManager()
    21. //     {
    22. //         for (int i = 0; i < 1000000; i++)
    23. //         {
    24. //             EntityManager.CreateEntity(ComponentType.Create<TestIterationComponent>());
    25. //         }
    26. //     }
    27.  
    28. //     [BurstCompile]
    29. //     struct Job : IJobProcessComponentData<TestIterationComponent>
    30. //     {
    31. //         public void Execute(ref TestIterationComponent c)
    32. //         {
    33. //             if (c.Value == 999)
    34. //             {
    35. //                 c = new TestIterationComponent { Value = 999999999 };
    36. //             }
    37. //         }
    38. //     }
    39.  
    40. //     protected override JobHandle OnUpdate(JobHandle inputDeps)
    41. //     {
    42. //         for (int i = 0; i < 20000; i++)
    43. //         {
    44. //             EntityManager.SetComponentData<TestIterationComponent>(components.Entities[i], new TestIterationComponent { Value = 1 });
    45. //         }
    46. //         var job = new Job { };
    47. //         return job.Schedule(this, inputDeps);
    48. //     }
    49. // }
    50.  
    51. struct CDFEComponent : IComponentData { public int Value; }
    52. public class CDFEPollSystem : JobComponentSystem
    53. {
    54.     [Inject] ComponentDataFromEntity<CDFEComponent> fromEntity;
    55.  
    56.     NativeArray<Entity> entities = new NativeArray<Entity>(1000000, Allocator.Persistent);
    57.  
    58.     override protected void OnCreateManager()
    59.     {
    60.         for (int i = 0; i < 1000000; i++)
    61.         {
    62.             var e = EntityManager.CreateEntity(ComponentType.Create<CDFEComponent>());
    63.             if (i < entities.Length)
    64.             {
    65.                 entities[i] = e;
    66.             }
    67.         }
    68.     }
    69.  
    70.     [BurstCompile]
    71.     struct SetEventJob : IJobParallelFor
    72.     {
    73.         public NativeArray<Entity> entities;
    74.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<CDFEComponent> fromEntity;
    75.         public void Execute(int i)
    76.         {
    77.             fromEntity[entities[i]] = new CDFEComponent { Value = i };
    78.         }
    79.     }
    80.  
    81.     [BurstCompile]
    82.     struct PollEventsJob : IJobProcessComponentData<CDFEComponent>
    83.     {
    84.         public void Execute(ref CDFEComponent c)
    85.         {
    86.             if (c.Value == 999999)
    87.             {
    88.                 Debug.Log("actually works");
    89.                 c = new CDFEComponent { Value = 999999999 };
    90.             }
    91.         }
    92.     }
    93.  
    94.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    95.     {
    96.         var setJob = new SetEventJob { entities = entities, fromEntity = fromEntity };
    97.         var pollJob = new PollEventsJob { };
    98.         var handle = setJob.Schedule(entities.Length, 0, inputDeps);
    99.         return pollJob.Schedule(this, handle);
    100.     }
    101. }
    I need to rethink my life.
     
    Last edited: Dec 19, 2018
    NotaNaN, JesOb and Spy-Shifty like this.
  39. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    That's pretty much what I did in my BatchSystem except I just used chunk iteration in the job tied to the known entities (in case the event was created separately as well)

    Setting by ref is by far the fastest way. It gets the full benefits of memory layout and the works.
     
  40. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    You mean you are creating events on separate entities? It's many times slower. Even with batching.

    upload_2018-12-19_12-52-1.png

    Red - Creating entity events
    Blue - Setting CDFE an polling
     
  41. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    There is nothing unexpected, 'ref' it's one of reasons why Unity suggests using exactly IJobProcessComponentData for many cases ;)
     
  42. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Not I'm setting the component values that way, I don't touch ComponentDataFromEntity like you are comparing to.
    For my batch system I create all the entities in a single call using the batch overload.
     
    illinar likes this.
  43. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    But that's not what surprises me at all, and that's not where the performance gain came from.


    Writing into ComponentDataFromEntity is many times faster than using SetComponent<>() Edit: that's what surprises me. It's literally thousands of times faster.

    upload_2018-12-19_13-19-35.png
    On the left 20,000 events with SetComponent() On the right 10,000,000 with CDFE. And they say CDFE is slow.

    Yes it takes up worker threads, but in terms of fps gain here this is it.

    Edit: Not thousands .. It takes 150 ms on worker threads. Fps grapf doesn't account for that.
     
    Last edited: Dec 19, 2018
    NotaNaN and Guerro323 like this.
  44. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Wait, I get it now. I didn't even realize that I was already avoiding SetComponent in my game code by using the refs, never came to my mind that it's possible to get rid of SetComponent altogether. Now I'm gonna add ComponentDataFromEntity in cases where it is more convenient. I might benchmark CDFE vs ref, at some point.

    So no SetComponent() is rarely necessary. Creating entities and archetypes and adding components can be avoided most of the time.
     
    Last edited: Dec 19, 2018
  45. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Of course it's faster you change values directly and immediatley, and it's equal 'ref' where you change value directly and immediatley :)
     
  46. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Who would suspect that such an innocuous sounding method as SetComponent() can be such a bad idea so often? :) So the best way to create objects is by instantiating them via an archetype (of course preferably batched) and then initiating via a chunk iteration on a job or IProcesscomponentData.
     
    Last edited: Dec 19, 2018
  47. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    To get them sorted! Just run a batch for chunks with shared SCD{raised=1} and get specific event data from a per-entity CD. Having same conditional results helps branch predictions.

    The Unity guys should code a SetSharedComponentDataForMultipleEntities(), I'm guessing mayor performance benefits from localizing source/destination chunks once, move entities to dest and compact source, everything instead of checking a hashmap per-instance, and compacting for each SetSharedComponentData call.

    I'm guessing those checks are there for a reason and removed in release?

    You're right, thanks for the correction. Was very sleepy, probably got confused with the asymmetry of the API (No "RemoveSharedComponent" for the Add/Set pair...)

    Your code is an inspiration!

    We all kindof do :D

    This is the fruit of the research! Thanks to everyone for being awesome.
     
  48. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    No. It's not safety checks, it's checking group SCD index and finding type in types dictionary.
     
  49. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    People, I've been testing with a different alternative inspired by tertle's BatchSystem (but single event type) with good results. After implementing illinar's last code on a set of tests I got, I'll compare everything on my own setup, I'll post results and code.

    The idea is the following: Ditch entities and components for events.

    QueueSystem:
    Has a NativeMultiHashMap Entity-->Event (big alloc, 100k elems... if it works ok for the "official" MeshInstanceRenderer I'll be fine...)
    Also has a NativeQueue to record Entities as the HashMaps don't have a way to extract keys :confused:.
    Would be better eventually to roll a custom struct to pass these around by pointer with helper methods to queue events.

    All systems running previously can inject QueueSystem to add events in the concurrent proxies of those containers.

    QueueSystem's update runs on the main thread, gets all (possibly repeated) entities from the queue but checks against a HashSet (managed) to get only unique keys. Then run the multimap for not-repeated entities' events and "execute" all events for that entity using ComponentDataFromEntitys.

    I've tried NativeQueue to NativeArray, native sorting with IComparer, and then skipping adjoined matching entities, but a HashSet was like 5x faster. All tested in the main thread.

    I've noticed performance bottleneck that has a lot to do with the HashMap size.
    For 100k elems I get 1-2ms roughly, for 10M elems I get 90ms... indifferent to using 10k/100k/1M entities each case (10M ents only for the later)


    I thought of using a NativeArray as a very very sparse set to jobify everything, although can't do parallel because NativeQueue doesn't have a concurrent Dequeue method (which was surprising). Would have to dequeue from main thread to native array, then to job, very messy. Or code my own multithreaded ADTs (zzz).

    Also my actual need for this system was to interface with classic-mode objects, responding to events only in the main thread seems like an OK idea for now.
     
    Last edited: Dec 19, 2018
  50. iam2bam

    iam2bam

    Joined:
    Mar 9, 2016
    Posts:
    36
    Ok, finally got some benchmarks, you can grab the project source here and try/tweak on your own.
    https://bitbucket.org/2bam/unityecseventbenchmarks

    To make it short:
    1. Polling is the fastest for one event type per entity.
    2. Followed by just queueing and processing directly via ComponentDataFromEntity without creating entities for events.
    3. Followed by tertle's BatchSystem to use ComponentSystem encapsulation convenience.
    4. Anything with a barrier is worthlessly slow (add/remove component/set SCD)
    BatchSystem for 2% event volume of entities runs same as polling. By event volume I mean approximate amount of entities generating events per frame. Tested with 100k and 10M ents, similar percentage.

    Also using a HashMultiMap for queueing by event-type is 10x slower for 100% event volume, but similar for 10% (tested at 100k ents). Could be worth to schedule different jobs for different events keeping modularity but avoiding ComponentSystem filtering altogether.

    10 million entities, one event per entity per frame (including around 13ms baseline) frame times:
    Poll 100ms, EvQueue 280ms, Batch 520ms

    I'm open to suggestions!
     
    Last edited: Dec 24, 2018
    illinar likes this.