Search Unity

Is Object Pool a valid optimization pattern in ECS?

Discussion in 'Entity Component System' started by Deleted User, Aug 15, 2019.

  1. Deleted User

    Deleted User

    Guest

    Object Pool is very common in games that are built with MonoBehaviours. Because instantiating game objects is costly.
    Do we need to create object pool in ECS? How costly is creating a new entity?
     
    KANIYO, deus0 and lclemens like this.
  2. supron

    supron

    Joined:
    Aug 24, 2013
    Posts:
    67
    I made some tests while I was writing an event system for my UI. I do not have profiler snapshots but I found out that creating entities and flagging with components (required to "mark" entities in a pool), are very similar in performance. Both operations require sync point and chunk operations (allocation/relocation). Creating entities in batch (EntityManager.CreateEntity(EntityArchetype, NativeArray<Entity>)) is even faster than pooling.
     
  3. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    ECS is awesomely fast at instantiating new Entities, you can use batch Operations for better performance.
    Generally Object Pooling in ECS is used when it's about hybred Entities which are Destroyed/Instantiated mulitple Times per Frame Like Bullets (Particale System).
    the Only Bad side about Destroy/Instantiate new entities is Sync Points but still Awesomely Fast.

    my Opinion !! (maybe wrong)
    in case you have Millions of pure entities that are Destroyed per frame, and other Millions of entities with same architype but their Components need to be reinitialized. you can modify their Components value By Chunk operations and not per Entity. eg by using CopyFromComponentDataArray<T>(NativeArray<T>, out JobHandle)
    logically modifying Components value on a lot of Entities is faster than Destrying them, creating new ones and modifying their Components value.
     
    lclemens, Orimay and (deleted member) like this.
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,266
    Pooling is only useful for reference types which are allocated on the GC-managed heap. This includes class types and System.Collections.Generic stuff. Generally you should avoid pooling unless you need these things. Unity.Rendering.Hybrid uses a pool for lights so that you can have millions of virtual lights in your scene but only a thousand or so active in a frame.
     
  5. rz_0lento

    rz_0lento

    Joined:
    Oct 8, 2013
    Posts:
    2,361
  6. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,775
    I use pooling for certain types of entities. But my main reason is, they got references in/to other entities and the generation require execution of some specific algorithms. Hence better for me generate fewer per frame. So in my case, pooling makes sense and I don't remove these entities over time. But when you got entities with only few components, just instantiate them.
     
    Opeth001 likes this.
  7. TheMomes

    TheMomes

    Joined:
    Jan 9, 2018
    Posts:
    2
    There are two problems that I can instantly see with this approach:
    1. Dynamic memory allocation on the heap is expensive, as the proper memory size must be allocated.
    2. Garbage Collection can cause lag spikes.
    3. Memory fragmentation. This is a nightmare in and of itself.
    If your game is something smaller that isn't running for long periods of time, or doesn't deal with lots and lots of entities at once, this is really no problem. Otherwise, this can cause some major issues.
     
  8. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    451
    ECS entities are not allocated in managed memory.
     
  9. TheMomes

    TheMomes

    Joined:
    Jan 9, 2018
    Posts:
    2
    Could you perhaps provide the source for this? I just want to verify, because I was going to spend a day or two experimenting and profiling this. Thank you very much for this information by the way, if this is verifiable you've saved me a few days! :)
     
  10. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    451
    All DOTS is based on NativeCollections and not using managed memory and reference types. An entity is not even a thing it is just an index to a stream of components. Before you start profiling something you need at least a basic understanding of it. Here is some material for a good start:
    https://docs.unity3d.com/Packages/com.unity.entities@0.11/manual/index.html
     
    lclemens and Deleted User like this.
  11. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    Made my day ;).

    reference
    If you don't know who this person is, look it up.
     
    lclemens likes this.
  12. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    761
    Quick question... Is the following a true statement? :

    If you're using a concurrent EntityCommandBuffer in an Entities.ForEach() loop to instantiate entities+components via prefabs with convert-to-entity, all the instantiations that were queued will execute in the main thread via a batching method.
     
  13. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Only if you're using batch overloads, otherwise they will be processed sequentially. But whole chain playback will be through burst code path (as playback itself is burst compiled function pointer) and it very fast, in opposite to EM calls (but this definitely will change I'm sure, for me, it's very reasonable, as Unity in process of implementing bursted systems, and I guess EM be aligned with the ECB in the context of the bursted code path, but if I'm wrong with this guess @Joachim_Ante will kicks me :D ). Also, EM calls currently call BeforeStructuralChange, BeginArchetypeChangeTracking (not a big deal) etc. for every call, ECB in opposite doing that only once before playback, and if between EM calls you schedule jobs - it will affect performance
     
    Last edited: Jun 21, 2020
    lclemens and MNNoxMortem like this.
  14. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Sorry for my necromancy, but did ECB ever get a batch function (native array) to instantiate? Was there a forum post I missed on best way to spawn 10k+ entities without the sequential? And congrats on the steam release eizenhorn!
     
  15. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    Is the main difference the fact that objects are classes and DOTS/ECS are structs two different types of data memory allocation?
     
  16. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,266
    No. And even if it did, it is unlikely it would perform much better as the rules for ECB make it hard to batch things in the internal layers. I did write an optimized command buffer that handles instantiation a lot faster: https://github.com/Dreaming381/Lati...intPlaybackSystem.md#instantiatecommandbuffer
     
    MagiJedi and deus0 like this.
  17. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Thank you, I am looking into this now, I remembered you mentioned it before and it slipped my mind. Will this solve the EntityCommandBuffer.Playback by using a custom ECB? This is my last optimization / bottleneck I have left. I can't use a whole framework as I have my own, but I'm wondering on the best practices with this.
    I moved some of my instantiation, SetComponent functions, into initialization systems. This way it avoids most queue on the ECB. These use EntityQuery's to push the data into the child chunk entities that spawn.
    Essentially we are just moving EntityManager.Instantiate commands to the simulation group with ECBs?
    Also my thoughts on ECB.Instantiate(index, prefab, nativearray), is instead of running Instantiate 10k times in a loop, we could run it once. This seems more efficient to me.
    upload_2022-2-18_18-55-9.png
     
  18. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,266
    I wrote an article on how it works here: https://github.com/Dreaming381/Lati...tion Adventures/Part 4 - Command Buffers 1.md
    A few people extract bits and pieces of the framework. If you have trouble identifying what pieces you need, just ask.

    Anyways, the biggest optimization only happens when the same entity is instantiated multiple times in batch. So what my custom command buffer does is group together all the commands instantiating the same source entity and use the batching instantiate command from within a Burst context during playback. Because it is an independent command buffer and has a different mechanism for initializing component values, the commands can be freely reordered making the batching much more effective.
     
    Tudor and deus0 like this.
  19. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Thank you Latios, amazing work. I will read through the article, the code and try to work out my own solution! Thank you for sharing. I think this was one of the draw backs of the current systems. I guess generic solutions normally provide the lesser efficient ones.
     
  20. Tudor

    Tudor

    Joined:
    Sep 27, 2012
    Posts:
    150
    Hi guys, it's me another necromancer.

    For compliance reasons and general good dev practice, it's often good to know just how much memory your game can possibly reserve, and to reserve it up front / at load time. You do this sort of thing when pooling, but how do you do this with plain ECS which loves random instantiation and moving stuff around with chunks?

    How can you even predict what's the total possible memory usage of your game, in a standard ECS game? At any point in time, multiple copies, moves, archetype and other database allocations can happen, on top of your explicit instantiation requests.. :)

    You could yolo, but then you get kicked off the playstation store :D
     
    Last edited: Mar 20, 2023
  21. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Run a game and profile it?

    Entity archetypes does not grown on their own, its caused by structural changes.
    And ECS is also way more efficient with memory management than you'd think. There are some rough edges, like allocating whole chunk for a single entity. But hopefully those will be addressed in future releases of Entities.

    Also, data and archetypes aren't taking that much space. Actual resources do.
    Stuff like textures, meshes, materials, sounds etc.

    One chunk is 16kb's. One texture could take megabytes depending on the quality and settings.
     
  22. Tudor

    Tudor

    Joined:
    Sep 27, 2012
    Posts:
    150
    Yes, profile the game. But my question was in general how do you guarantee constant memory usage, in a paradigm where you can't pool, and unity is the one who moves things around in memory based on what state changed on components and chunks etc.

    Say you were very strict, had to make a game that uses 1gb of ram, no more no less. Yes you know how many entities you are spawning, manage sync points etc, but you don't entirely know or entirely control what unity is doing with your memory when you change state by e.g. shooting an enemy. (maybe I don't know enough ecs wizardry yet)

    PS: How do y'all suggest you manage the memory of meshes and textures in relation to ECS? Do you pool those, but instantiate and destroy the ECS elements? You often don't want to increase or decrease the total number of resource instances available at a time.

    (I obviously know the difference between blittable and managed code)
     
    Last edited: Mar 21, 2023
  23. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Count how much entities you have through the queries.
    And restrict spawning new entities by adding respective logic to the systems.

    (if that's ever an issue. Profile first.)

    You don't. Resources are handled by the engine.
    What you can do is to load something, or unload something.

    Technically, you could go with addressables to pick which resources to load, but in most cases you don't need to.
    Unless doing content via distribution, etc.

    What you're confused right now is managed objects growing heap. ECS works differently.

    If simplified:
    Managed objects waste much more memory than entity does on average basis. That's because OOP constructs objects in a wasteful "human-like" form, where data and objects are separated by large memory jump distances. That's the nature of heap, which does not have one single allocated block, but rather lots of locations to store actual data.
    Not to mention data that aren't related to the game at all.

    With DOD data is tightly packed into chunks of 16KBs. Space for which is allocated and reused (as a single block).

    Imagine object pooling actually re-using not the object, but the whole memory space it takes. So there's no wasteful objects lingering when they aren't needed. E.g. when object is decommissioned back to the pool -> that's memory wasted, because they aren't actually needed, but still there.

    What you need is to read is this:
    https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/concepts-archetypes.html

    Basically, case where you run out of memory space would happen with managed objects sooner than with entities. As for the resources -> they're loaded once and data is a pointer to that resource.
    So there's no data duplication (unless introduced via user bugs)

    If you want to unload resources - you can always do it like it was before.
    E.g. via Resources.UnloadUnusedAssets. As long as there are no references - they will get unloaded.
     
    Last edited: Mar 21, 2023
  24. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,775
    Entities are your least of memory problem.
    You need to keep in mind GC generation by managed data type, media assets that you use, like textures, animations, meshes, etc.
     
    apkdev, Sirius64 and Tudor like this.
  25. Tudor

    Tudor

    Joined:
    Sep 27, 2012
    Posts:
    150
    I was concerned with memory management even on the stack, when e.g. you create 1000 entities, then destroy them, but you know you will need them again later, so why did you just destroy them?

    And also the sort of accounting problem (which used to be quickly solved by pooling or buffers), of game's memory growing and shrinking all the time, from various sources. E.g. at one point you can have 1000 enemies and 10 bullets, or 1000 enemies and 1000 bullets and 1000 chickens -- so if you just had constant buffers of 1000 of each, you knew exactly what you're working with. (this buffer example obviously wastes memory, but still)

    But you're right, ECS data is very small, and it's fine that you keep deleting, re-creating it, re-sorting it. It's fast enough. (though not always convinced about the sorting part, even with a ECB sync it can be excessive)

    But do you have any examples of good ways to pool many instances of non-ecs things (e.g. GUI elements, meshes), and associate them to ECS things that keep getting created and destroyed?
     
    Last edited: Mar 21, 2023
  26. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Problem you don't realize here is - those aren't objects.

    You don't manually set them one by one. What happens is if simplified, is pretty much memcpy.
    Which is fast. Like absurdly fast.

    So setting up pooling & performing actions to reset / re-initialize data state one-by-one would require way more ops than one glorified memcpy.

    Try running a simple perfomance test. Entity instantiate 1000 entities.
    And then do the same, but with a separate job that runs over each of those entities resetting their state to initial value.
    More data it gets -> slower it gets. [when iterating one-by-one]

    Structural changes aren't the case here. They're slow in cases where archetypes change often. (see as in enable / disable case, where enable mask should be used instead)

    Doing one big structural change op is faster than doing lots of small ones.

    Managed objects can be pooled as it was.
    For example:
    - Mark entity somehow upon spawning to notify other systems that it requires [new] pooled object.
    - Grab pooled prefab by id (int, uint, FixedString whatever, just make sure to prepare lookup before upon game load).
    - Pool & assign ref to the entity. (This has to be done in managed system [SystemBase] + AddComponentObject).
    - For the cleanup of "destroyed" object entities - use ICleanupComponentData to notify cleanup system which entities should have their objects decommissioned back to the pool.

    Entity query would be ComponentData type (.Exclude) + CleanupComponentData type;
    Cleanup components aren't removed upon entity destruction, so until CleanupComponentData is not removed - entity exists.

    Different id assigned to the object by the pool can be used to relate object to pool. So in the end you could run a job to grab all destroyed object ids -> put them into list -> decommission them one-by-one on the main thread.

    Alternatively, you could "mark" objects as "destroyed" (instead of directly destroying entity) and run logic directly on the ComponentObject [PooledObject] query, decommissioning objects one-by-one on the main thread.

    https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/components-cleanup-introducing.html

    However, there's one more thing.
    Visual representation layer can be aproached differently. Instead of packing everything visual related to the MonoBehaviour going hybrid approach with pooling, you could write custom systems that allow rendering based on what data there is. This is what Entities Graphics do for example.

    Same can be applied to UI, VFX, SFX etc. Though it requires each case to be handled separately.
    Utility vs performance.
     
    Last edited: Mar 21, 2023
  27. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,775
    You need to profile and test for your specific use case.
    If you don't profile, you will always guess and run with assumptions.