Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

DynamicBuffer Memory Layout

Discussion in 'Entity Component System' started by JooleanLogic, Nov 18, 2018.

  1. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    I was going to reply this to another thread here but I figure it's better in a new thread.

    My query is whether DynamicBuffer's are wholly/partially allocated into archetype chunk space in the default situation. I get that they can be stretchy and heap allocated, but I want to know how they operate in the situation where you just want a fixed array of n components.
    For example if you wanted an array of 4 wheel entities per car entity where n is never going to change.

    In the following example where InternalBufferCapacity(4) is not going to exceed the chunk size, are they initially allocated directly into archetype chunk space just like components are?

    Code (CSharp):
    1. [InternalBufferCapacity(4)]
    2. public struct A : IBufferElementData
    3. {
    4.     public int Value;
    5. }
    6.  
    7. var archetype = EntityManager.CreateArchetype(typeof(A));
    8. Entity entityA = entityManager.CreateEntity(archetype);
    9. Entity entityB = entityManager.CreateEntity(archetype);
    How is this laid out in memory?
    Scenario A - everything in the archetype chunk or
    Scenario B - just the DynamicBuffer (8 bytes) in the archetype chunk and the BufferHeader and components are stored in a seperate memory block.
    Code (CSharp):
    1.  
    2. // Scenario A
    3. DB[BH[A,A,A,A]], DB[BH[A,A,A,A]], ...   // Archetype chunk component stream
    4.  
    5. // Scenario B
    6. DB, DB, ...                               // Archetype chunk component stream
    7. BH[A,A,A,A], BH[A,A,A,A], ...           // Different memory block component stream
    DB=DynamicBuffer, BH=BufferHeader

    DynamicBuffer internally just contains a pointer to a BufferHeader which contains a Pointer to the actual data (relevant source code below)
    Code (CSharp):
    1. struct DynamicBuffer<T> where T : struct{
    2.    BufferHeader* m_Buffer;
    3.  
    4.    public T this [int index] {
    5.        get {
    6.            return UnsafeUtility.ReadArrayElement<T>(BufferHeader.GetElementPointer(m_Buffer), index);
    7.        }
    8.    }
    9. }
    10.  
    11. struct BufferHeader {
    12.    public byte* Pointer;
    13.    public int Length;
    14.    public int Capacity;
    15.  
    16.    public static unsafe byte* GetElementPointer(BufferHeader* header) {
    17.        if (header->Pointer != null)
    18.            return header->Pointer;
    19.  
    20.        return (byte*) (header + 1);   // First address after this header?
    21.    }
    22. }
    The code at line 20 reads to me that if DynamicBuffer.BufferHeader.Pointer is null (possibly initial state), then DynamicBuffer[0] returns the first piece of memory after the BufferHeader. BufferHeader expects the component stream is going to start directly after this header, which I assume is so it can be laid out in a chunk stream.
    Is that right?

    Now lets say I dynamically add an element to one of the buffers.
    Code (CSharp):
    1. DynamicBuffer<A> bufferA = EntityManager.GetBuffer<A>(entityA);
    2. bufferA.Add(new A());
    This causes BufferHeader to malloc new memory which it assigns to BufferHeader.Pointer.

    So if you don't resize buffers after initialisation, they're going to be contiguously packed in either the archetype chunk (or some other chunk linked mem block) just like other components, and copied around just like other components?
    If you resize buffers, then you're going to end up with non-contiguous per entity heap allocations and the initially allocated chunk space is going to remain allocated but unused?

    I haven't looked down the rabbit hole into how chunks and ComponentTypes are actually initialised yet so I don't know how or where the code is that initialises the DynamicBuffer. Feel free to point me somewhere.
     
    hellengoodd and RaL like this.
  2. bryanmcnett

    bryanmcnett

    Unity Technologies

    Joined:
    Aug 16, 2018
    Posts:
    12
    InternalBufferCapacity refers to the number of Buffer elements that is stored inside the Chunk alongside all the other component data. If any particular Entity's Buffer exceeds its InternalBufferCapacity, all of its elements are stored outside the Chunk, in a piece of memory that is allocated for that purpose.
     
  3. hellengoodd

    hellengoodd

    Joined:
    May 31, 2017
    Posts:
    51
    You didn't answer all the question...
     
  4. hellengoodd

    hellengoodd

    Joined:
    May 31, 2017
    Posts:
    51
    I have the same question. Have you found the answer?
     
  5. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Well I've seen worse necro's :)
    Is there any specific bit you're after?

    A buffer contains the header (16 bytes) and the data.
    The header is always stored in the chunk.

    Chunk Allocated
    If you specify InternalBufferCapacity(n>0) and it's small enough to fit in the chunk, then the buffer data will be stored in the chunk. Header followed by n*components.
    If you add elements beyond the capacity of the buffer, then the data is moved to the heap but the space it takes up in the chunk remains the same.

    Heap Allocated
    If you specify InternalBufferCapacity(0), then the buffer data is stored on the heap, outside the chunk..
     
    hellengoodd likes this.
  6. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    954
    Yeah, there are worse necros. :)

    You can write a "this []" accessor method for any IComp where you store your elements one by one. This has perfect memory layout and no overhead of the BufferHeader.

    Setting InternalBufferCapacity is in a weird state because you reserve additional memory that are not needed. Chunk space is already pretty tight and for chunk buffer data, a pointer, the length and the capacity (could be hardcoded in codegen) is not needed.

    So, I'd even argue, the whole BufferHeader is not needed. But it's written in a generic way and doesn't take advantage of code generation so we are left with data that makes no sense with a fixed limit.

    Suggestion:
    I'd suggest to get rid of InternalBufferCapacity and instead introduce a new buffer element: FixedChunkBuffer for exactly when we have a fixed size.
    DynamicBuffer then is always in heap memory with the improvement that the BufferHeader is moved outside of the Archetype.
     
  7. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    I agree, buffer is odd in that it has three implementations each of which can greatly affect performance.
    However as a generic solution, it covers all the bases (chunk/heap/dynamic/fixed) and those after performance can choose the best configuration for their purpose.
    Is that FixedChunkBuffer with or without a length? Cos they're two different use cases again.
    Either one of which is better than DynamicBuffer for said purpose but they've already been asked for years ago. I recall there being some reason for it not being feasible but maybe that's changed now with code gen.
    Even if DynamicBuffer was heap only, I think the header would still have to be in the chunk to facilitate structural changes. I can't see these becoming heap only though because as it stands, there is a use case for chunk based DynamicBuffer that isn't catered for by anything else.
     
    Last edited: Apr 30, 2022
  8. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    954
    That's my point I guess, it's too generic.
    FixedChunkBuffer would imply fixed size. No length or capacity.
    Something that would have variable length could be implemented in a DynamicChunkBuffer that has a length and capacity.
    Yeah, I think codegen opens up now pathways for that problem.

    The header could be minimized to just the pointer in the chunk so that length and capacity lives somewhere else in memory, together with the buffer data.