Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're just starting to look into custom allocators, you probably have this thought: "Why would I bother writing my own? The system allocator is written by geniuses at Microsoft or GNU; surely they've already optimized it to the limit."

It's a reasonable assumption, but it's fundamentally wrong. The system allocator is a general-purpose tool. It has to handle a request for 16 bytes and a request for 16 megabytes with equal stability. To do that, it uses complex bookkeeping, locks for thread safety, and search algorithms to find a free hole in the heap that fits your request. When you're doing this 10,000 times a frame in a game loop or a high-frequency trading app, that "general purpose" logic becomes a massive bottleneck.

The Myth: System Allocators Are Always Fast Enough

Let's look at a concrete scenario. Imagine you're building a particle system. Every frame, you spawn 5,000 Particle objects and destroy them shortly after. If you use std::vector<Particle> with the default allocator, you're hitting the heap constantly. Even worse, if those particles are allocated individually via new, you're scattering them across your RAM.

// The "slow" way: hitting the general-purpose heap constantly
for(int i = 0; i < 5000; ++i) {
    Particle* p = new Particle(); // System call, lock acquisition, bookkeeping
    particles.push_back(p);
}
// ... later ...
for(auto p : particles) delete p; // More bookkeeping, fragmentation risk

In this case, the system allocator isn't slow because the code is bad; it's slow because it's doing too much work. It's trying to be fair to every other part of your program. When you write a custom allocator, you're telling the compiler: "I know exactly how I'm going to use this memory, so stop guessing and just give me a slab of it."

The Reality: Managing Memory vs. Managing Objects

One thing that trips people up when they first implement std::allocator is the distinction between allocation and construction. In a normal new call, these two things happen at once. With custom allocators, they are decoupled.

  • allocate(): This just grabs raw, uninitialized bytes. It doesn't know about constructors. It's like buying a plot of land.
  • construct(): This is where the object is actually born (using placement new). This is like building the house on that land.

I've seen plenty of devs try to call the constructor inside allocate(). Don't do that. If you do, you'll break the way STL containers work, and you'll likely end up with memory leaks or double-construction bugs that are a nightmare to debug.

The Arena Strategy: Trading Flexibility for Raw Speed

The most effective "first" custom allocator I ever used was the Arena (or Linear) Allocator. The logic is dead simple: you allocate one giant block of memory upfront, and when someone asks for more, you just move a pointer forward. You don't "free" individual objects; you just wipe the whole arena clean at the end of the frame.

template <typename T>
class ArenaAllocator {
    T* buffer;
    size_t offset = 0;
    size_t capacity;

public:
    using value_type = T;

    ArenaAllocator(size_t size) : capacity(size) {
        buffer = static_cast<T*>(::operator new(size * sizeof(T)));
    }

    T* allocate(std::size_t n) {
        if (offset + n > capacity) throw std::bad_alloc();
        T* result = &buffer[offset];
        offset += n;
        return result;
    }

    void deallocate(T* p, std::size_t n) noexcept {
        // We do NOTHING here. That's the secret.
        // We reclaim everything at once by resetting the offset to 0.
    }
    
    void reset() { offset = 0; }
};

By ignoring deallocate, we've turned a complex O(log n) search for a free memory block into a simple O(1) pointer addition. It's incredibly fast, and because the memory is contiguous, your CPU cache will love you.




📋 Practical Task

Exercise: Implementing a Fixed-Size Pool Allocator

The Arena allocator we discussed is great for short-lived data, but it's useless if you need to delete objects in a random order. For that, we use a Pool Allocator.

Your task is to implement a PoolAllocator for a specific object type (e.g., struct Node { int data; Node* next; };). Instead of a simple pointer move, your allocator should maintain a "free list" of available slots.

Requirements:

  • Pre-allocate a block of memory capable of holding 1,000 Node objects.
  • Implement allocate() so that it pops a pointer from the free list.
  • Implement deallocate() so that it pushes the returned pointer back onto the free list for future reuse.
  • Verify that allocating 1,000 nodes, deleting 500 of them, and then allocating 500 more does not exceed the original pre-allocated memory block.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.