Search Unity

Order of entities

Discussion in 'Entity Component System' started by PlayingPolicy, Mar 30, 2018.

  1. PlayingPolicy

    PlayingPolicy

    Joined:
    Dec 20, 2013
    Posts:
    15
    One thing usually glossed over (if not outright ignored) on the topic of data-oriented design is the question of exactly which order/sequence you store your entities in. We know we want them stored contiguously, but that still leaves N! ways to skin a cat. You have to choose, whether consciously or accidentally.

    It's important because it affects the spatial and temporal locality of memory accesses between entities, whether directly or as mediated by spatial data structures (etc). Games feature a bunch of entities that don't just inertly transform their own states based on their respective initial states. Entities interact with each other.

    Now, the obvious options of entity order I've run into in past projects are very much intertwined with the mechanism that's ensuring contiguity. The options follow:

    Swap Trick order. You move the last entity to the destroyed entity's old position (or some fancy batched variant of this). It's doubtful that the swap trick can be beaten for efficiency of removals per se, but it leaves the entities in some crazy order that is impossible to reason about, and probably impossible to exploit for efficiency. Due to anxiety over not knowing exactly what my code was doing, I stopped using the swap trick altogether some years ago.

    Collapse order (anyone know a standard name for this?). You write the entity data back over itself based on an "alive" mask, with all dead entity data getting overwritten. This is my go-to for simple cases. It's efficient enough as you only eat the bandwidth cost of iterating the data once. Assuming you're inserting at the back, it leaves the entities in order of age. Long-lived entities cluster toward the beginning of the data, and short-lived ones toward the end. Makes some sense.

    Sorted order. You just define some key based on parts of the entity state that are interesting to systems, such as position, prototype index, FSM state etc. Then, you sort the entities every frame. Now, I've rarely had to break out the big guns and go this route, but it has some good properties. Sorting is the only way that completely decouples contiguity-maintenance from order. It's super flexible and reasonable.

    Nobody will call sorting "efficient" out of the box; however, the cost of sorting itself is tolerable on modern PC hardware at the sane limit of real-time entity processing, which I take as ~200,000. Re-arranging all the data can be more painful. There, you suffer the structure-of-arrays approach to some extent. Still doable in many cases. Looking at it another way, sorting trades a one-time cost for greater efficiency in any system that pulls together disparate entities. That guarantees some break-off point where sorting comes out ahead. (assuming you're not somehow cramming everything in L1 cache)

    Anyway, it's too late and this post is too long, so for now some questions about the ECS and job system:
    • exactly what order are entities stored in in the ECS?
    • why was this order chosen?
    • are there different options planned for direct support?
    Cheers!
     
    nous likes this.
  2. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    * We lay out component data in chunks of 64kb blocks with matching archetypes
    * We use Swap trick order (Within a chunk linear tight packing guranteed)
    * Component data is being moved to different chunks when adding / removing entities
    * ComponentDataArray<> / ComponentGroup makes zero gurantees on the actual ordering of your entities except for that its deterministic. But generally the indices are not stable. (E.g. Adding a component to an entity, will change the index in ComponentDataArray of that entity and likely of another one) The only stable id is the "Entity" ID.

    We do this because:
    * Want to gurantee 100% linear layout within a chunk to allow zero overhead innerloops for processing entities / component groups
    * Fixed block size makes it so we can easily reuse memory between all entity data
    * Instantiate is within 10% margin of memcpy speed


    We do want to add support for sorted / hierarchical / topological walking of the data. (This is not implemented yet natively however you can do it manually using ComponentDataFromEntity<>)
     
    GilCat and Zoey_O like this.
  3. PlayingPolicy

    PlayingPolicy

    Joined:
    Dec 20, 2013
    Posts:
    15
    Cool. Please correct me if I'm wrong: it's essentially a bucket sorted order now, with one bucket per archetype. So to lay up jobs which process disparate entities, it can be prudent to do lots of filtering/specialization via components, to keep your working set small. And the same technique is how you avoid heavy branching (or excessive computations merged with predication) in jobs.

    It leads to another question:
    Do you eat a basically random memory indirection every time you access data via stable Entity ID vs ephemeral Component Index? If so, you should avoid the former unless you really need it, e.g. maintaining references to entities over several frames. I think I see this pattern in the StressTesting demo code.

    BTW, I wish this stuff was a major topic in the documentation. Data-oriented design isn't new to me. I'm no hotshot expert, but I've been exposed to the literature from the beginning, and it heavily influenced the way I program. Probably more influential than any other broad idea- all programs really are just "reasonable data transformations". Being exposed to those ideas young helped me transition to more of a graphics programmer (GPU stuff is more intuitive to me now).

    Most of the documentation seems like straightforward best practices, even where it's not some specific thing I've ever put to practice. But once you get into the thorny & intertwined issues of compactness, sequence, and maintaining references over time, you're hitting hard problems that necessarily involve serious trade-offs. Programmers need to know exactly which trade-offs were chosen in the underlying system, so they can consciously build their systems on that foundation. Disclaimer: this rant is a request! I know you guys are just getting started on the documentation.

    Thanks a lot.
     
  4. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Yes. Filtering via components (we call the component tags - an empty component) works amazingly well.

    Also you can use shared component data, it works really well for more dynamic things. Eg. for some streaming proc-gen sample i am working on I am using ISharedComponentData with it containing the grid position. This forces all the data together in a chunk.

    https://github.com/Unity-Technologi...content/ecs_in_detail.md#shared-componentdata
     
    MINORLIFE likes this.
  5. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Indeed. Accessing something by Entity is an extra indirection. (Salted table, well optimized)
    But more importantly accessing something by Entity ID implies random access to the actual data!

    ComponentDataArray / ComponentGroup / IJobProcessComponentData on the other hand offers guranteeds linear memory access. Which is awesome for Bursts ability to auto-vectorize code.

    Check out this inner-loop from IJobProcessComponentData. Since the data is guranteed linear layout. It means that the the burst compiler can then inline Execute and auto-vectorize across multiple component types. Still blows my mind every time I look at it...

    Code (CSharp):
    1.  
    2. public static unsafe void ExecuteInnerLoop(ref JobStruct_Process2<T, U0, U1> jobData, int begin, int end)
    3.             {
    4.                 ComponentChunkCache cache0, cache1;
    5.              
    6.                 while (begin != end)
    7.                 {
    8.                     jobData.Iterator.Iterator0.UpdateCache(begin, out cache0);
    9.                     var ptr0 = UnsafeUtilityEx.RestrictNoAlias(cache0.CachedPtr);
    10.  
    11.                     jobData.Iterator.Iterator1.UpdateCache(begin, out cache1);
    12.                     var ptr1 = UnsafeUtilityEx.RestrictNoAlias(cache1.CachedPtr);
    13.  
    14.                     var curEnd = Math.Min(end, cache0.CachedEndIndex);
    15.  
    16.                     for (var i = begin; i != curEnd; i++)
    17.                     {
    18.                         ref var value0 = ref UnsafeUtilityEx.ArrayElementAsRef<U0>(ptr0, i);
    19.                         ref var value1 = ref UnsafeUtilityEx.ArrayElementAsRef<U1>(ptr1, i);
    20.                         jobData.Data.Execute(ref value0, ref value1);  
    21.                     }
    22.  
    23.                     begin = curEnd;
    24.                 }
    25.             }
     
    Last edited: Mar 31, 2018
    MINORLIFE, NotaNaN, RaL and 2 others like this.
  6. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    @Joachim_Ante So in the future, it will be possible to force certain archetypes to be sorted in a custom and maybe also fixed order (collapse order)? It would definitely make sense in cases where linear memory itself gives no performance benefits because we don't need to iterate through the component data, but instead, we need to navigate within it as if it was a 2D or a 3D array (e.g. mesh generation for voxel objects, A* pathfinding), or when we just need to access a specific subset. Also usually data like that can be pretty static, no entities destroyed or added, and some of them can instead be treated as empty.

    Edit: I just figured out how to use arrays on entities, this solves a lot of my problems in theory, except arrays bigger than 10,000 do not work (https://forum.unity.com/threads/lists-of-things-collections-arrays-groups.524532/). Will it be possible to support dictionaries on entities too?
     
    Last edited: Apr 1, 2018
  7. PlayingPolicy

    PlayingPolicy

    Joined:
    Dec 20, 2013
    Posts:
    15
    ^ Bear in mind that due to the way memory paging and caching work, there is bound to be some scale at which logical memory locality in your program has zero correspondence to physical memory locality in the memory system. So some degree of hugeness of a voxel array is unnecessary for good locality. That's one reason why "chunking" is universal to any non-trivial voxel application. Try storing an appropriately-sized chunk of voxels per entity.


    That raises just one more clarification. The latter part about random access to data is the indirection I alluded to. Why is there an additional indirection on merely accessing the map from Entity ID to Component Index? The obvious way is just a table storing an integer per Entity, in Entity order, which changes just as often as the bucket changes. Unless I'm missing something peculiar to this new ECS. Cheers!
     
    illinar likes this.
  8. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    >Will it be possible to support dictionaries on entities too?

    We are working towards an API to support safe containers on entities.
     
    recursive and FROS7 like this.
  9. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    So essentially the work is:
    * Lookup in salted array / table. This gets us to the archetype / chunk
    * Search in archetype for which offset the component lives in this archetype
    * return memory for that

    So its roughly similar cost to GameObject.GetComponent (Its faster due to some smart caching & no C# -> C++ transition) But really if you care about performance its using ComponentDataFromEntity & EntityManager.GetComponent for only the cases where its absolutely necessary. Default is to process in batch with zero overhead.
     
    RaL and illinar like this.
  10. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Makes a lot of sense.