6

I'm going through some code from this article about ECS-systems in game programming and trying to understand it, and something I'm seeing a lot is using heap memory in places where it seems like there is no benefit in doing so. Take this as an example:

class ECS
{
public:
    void someFunction()
    {
        archetypes.push_back(new Archetype);
    }
    ~ECS()
    {
        for(Archetype* a : archetypes_)
        {
            delete a;
        }
    }
private:
    std::vector<Archetype*> archetypes_;
};

This is the only way that the archetypes are manipulated in memory in the code. The rest of the code just uses them.

Why would you ever choose to use allocated memory for this? I see this often in code and it seems to me like using heap memory just because it feels like the right thing to do, and not actually considering if it's the appropriate place for it. std::vector already uses heap memory behind the scenes so why not just copy a stack variable into the vector when we want to add a new archetype, and let the vector handle the allocation?

class ECS
{
public:
    void someFunction()
    {
        archetypes.push_back(Archetype());
    }
private:
    std::vector<Archetype> archetypes_;
};

Or are there valid reasons for using heap memory in cases like this?

2
  • 4
    Archetype is probably intended to be a base class with virtuals. Your second example would slice the objects, killing polymorphism.
    – Mat
    Nov 6, 2021 at 22:15
  • @Mat This was also my first thought. But Archetype does not seem to be derived further in this article.
    – Christophe
    Nov 7, 2021 at 17:34

2 Answers 2

10

There may be several reasons for proceeding this way, and in particular:

  • Polymorphic container: the vector keeps objects of different classes sharing a same base class and uses polymorphism. A vector of base class objects would lead to slicing.
  • Share objects: pointed objects are shared between several vectors/objects. Vectors containing objects instead of pointers, would keep their own independent object copies.
  • Performance: For example, vectors may grow and move data to another place if needed. Moving pointers is much faster than moving very large objects.

Use of new and delete creates additional burden and risks. Since C++11, the use of the safer unique_ptr or shared_ptr is usually preferred: they avoid the memory management hassle.


Having read the full article and contacted the author, it appears that the manual memory management here is for performance reasons: the code organises the objects according to a specific memory layout. Moreover some toolchains used when targeting different game consoles do not always optimize well smartpointers. The game industry would therefore favor raw pointers. Thus the manually fine-tuned memory management. (I thank by the way the author in case he reads this answer for the responsiveness and clarity)

8
  • As long as ownership of the Archetype instances is in the vector holding all of them, and as long as that vector has sufficiently encompassing lifetime over all uses (which is likely), then the vector can hold them with unique_ptr and every use can simply use raw pointers if the vector can't grow (i.e., is reserved with sufficient space).
    – davidbak
    Nov 7, 2021 at 15:59
  • 1
    @davidbak thanks for the clarification. I didn't want to enter into the details of unique_ptr vs shared_ptr since this would be worth a separate questions (and tehre are already plenty of them). Meanwhile I read the article with more attention and observed that Archetype was not derived further, but that other elements enter into consideration. I updated the answer accordingly.
    – Christophe
    Nov 7, 2021 at 17:28
  • Are you sure that the author understood that you were referring to the archetype objects? Because the I'm fairly certain that the archetype objects are not organized according to a specific memory layout. The code does contain objects that are manually laid out next to each other using placement-new to preserve data locality, but the archetypes are not one of them. They are simply allocated using the normal new, referenced with a vector of pointers and then freed when the ECS system is destructed.
    – JensB
    Nov 8, 2021 at 17:04
  • @JensB I didn't ask specifically for Archetype. I asked in very broad terms, having seen the extensive manual memory management in the code, including placement new. The author explained the general performance constraints/reasons leading to the use of raw memory management and clarified that reuse of already allocated memory (placement new) is part of these practices. The memory layout was not mentioned by the author but it was explained in the article and it seems important as it may be relevant to other objects(thus my opening sentence "Having read the full article and contacted....").
    – Christophe
    Nov 8, 2021 at 19:02
  • 1
    @Christophe The question was badly phrased on my end. I should have made it clearer that I was referring to this specific use of heap allocation, and probably included a bit more code from the article. You're completely right about the the pointers in the map becoming invalidated when it resizes itself - I had not thought of that at all. The reason I commented on your answer is because I was confused about the author justifying the heap usage by pointing to data locality, which was not present with the archetypes. But I understand now that you didn't ask him/her specifically about archetypes.
    – JensB
    Nov 8, 2021 at 20:21
0

The most common reason for this that I see from an ECS perspective is to avoid invalidating pointers on subsequent insertions to the vector sequence. Components will often want to point to each other (they could do it via indices rather than pointers, although it's a bit less convenient in code). For example, motion parenting as needed to do FK/IK will want components to store some sort of link (pointer or index) to child components to link them together (assuming you don't use a completely separate tree structure living outside your ECS). You can make up for the heap allocation overhead of operator new/delete with an efficient allocator using memory pools, like a freelist with constant-time allocations/deallocations.

Personally, my performance requirements are so tight though that I don't do this. Our ECS can often have millions of entities inside, and it helps to use 32-bit indices here rather than 64-bit pointers on 64-bit architectures and avoid the extra heap allocations (even using memory pools). We use the ECS on the server-side for an MMO with a massive world. But if it's like 10k entities rather than millions, I think using a sequence of pointers here, dynamically allocated, is more productive than integers storing a relative index to the vector. Ours has the programming burden of having to have access to the ECS scene together with the index to get to a particular component from a source component referencing it with an array lookup rather than direct pointer access but it was a productivity compromise we found was worth it after measuring before and after.

But the number one reason I see to do this is to avoid invalidating pointers as std::vector<T> will do on insertions if T is not a pointer. I don't think polymorphism typically applies here with a need to store base pointers in an ECS like some base Component type that components inherit with overrides to virtual functions since an ECS generally stores and fetches (casts) component types directly of a particular type. If polymorphism is used here to make the code a bit more type-safe, then it's typically applied on a component container level (i.e, ComponentList base type) rather than per component in ones at least slightly concerned with efficiency. I could see the ability to move pointers around as a valid reason if the ECS is designed such that it shifts the contents of its containers around for more efficient component queries, although it makes less sense to me in archetype systems that fundamentally want to avoid even the inefficiencies of using variable strides jumping all over memory to iterate through components of multiple types (something which can still incur many cache misses even if you sort the pointers in advance by address for multi-component-type sequential access patterns). They usually want to minimize even basic cache misses with multi-type component queries so they usually rearrange and cache memory at the pointee level and not just pointer/index level to optimize queries.

So I think the most common reason here is to avoid pointer invalidation on insertions, which ultimately translates to programmer convenience. At least after implementing multiple ECS architectures and looking at a bunch out there, that would be the number one reason I'd see to do this as a productivity compromise at some cost to efficiency (which might not be a big cost in some game that only has, say, 10k entities on average in a given scene... although it might not even be worth bothering to implement archetypes in those).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.