Hello! Welcome to our next lesson.
In the previous session, we separated the concepts of memory allocation and object construction, mastering placement new as the tool to construct objects in pre-allocated memory. We also introduced the concept of an Arena, or Linear Allocator, as a high-performance memory strategy.
Today, we move from concept to reality. We are going to build our first key component for the telemetry engine. The learning outcome for this lesson is to implement a simple linear allocator with bump pointer allocation. By the end of this lesson, you will have written a reusable C++ class that provides blisteringly fast memory allocation, forming the backbone of our future data capture system.
Recapping the Core Idea
A linear allocator, also known as a bump allocator or arena, is the simplest and often fastest type of memory manager. Its core idea is incredibly straightforward, which is the source of its power.
To get a high-level feel for how they work and why they are so useful, let's watch two short videos. The first gives a simple analogy and explains the basic mechanism, while the second frames its limitations as a powerful feature for managing object lifetimes.
First, watch this video from Nic Barker. It introduces the linear allocator and uses the function call stack as a perfect real-world example of where this allocation strategy shines.
Please watch from 02:44 to 04:57 to see the introduction to the linear allocator. Then, watch from 06:19 to 09:41, which explains why this pattern is so effective for data with LIFO (Last-In, First-Out) lifetimes, like function calls.
Arenas, strings and Scuffed Templates in C
Next, this video from VoxelRifts explains the same concept from a slightly different angle, emphasizing cache-friendliness and how arenas enforce clear object lifetimes.
Please watch from the beginning (00:13) to 04:27. Pay close attention to how it contrasts arenas with malloc/free and introduces the idea of managing memory for different lifetimes by using different arenas.
To summarize the key principles:
- Allocate a big block once: We request a large, contiguous chunk of memory from the operating system a single time.
- Bump a pointer: Allocations are served by simply returning the current address of an "offset" or "bump" pointer and moving it forward. This is just pointer arithmetic, not a system call.
- Reset to deallocate: Individual deallocations are not supported. To free memory, we reset the entire arena by moving the offset pointer back to the beginning.
This strategy is a perfect match for our telemetry profiler. Each instrumented thread will have its own linear allocator. During a single frame of the application, it will record thousands of events, each allocation just a quick pointer bump. At the end of the frame, a consumer thread will read all the events, and the allocator can be instantly reset for the next frame.
Designing Our LinearAllocator Class
Let's start by outlining the C++ class we're about to build. It needs to manage the memory block and the allocation process.
Data Members:
- A pointer to the start of the memory block. We can use
char*or, more modernly,std::byte*. - The total size (capacity) of the block in bytes.
- The current offset from the start, tracking how much memory has been used.
Member Functions:
- A constructor that takes a size and allocates the main memory block.
- A destructor that deallocates the block.
- An
allocate()method that takes a size and an alignment requirement and returns avoid*to a valid memory location. This is the core "bump" logic. - A
reset()method that sets the offset back to zero. - We'll also make the class non-copyable and non-movable, as it's managing a unique resource (the memory block).
Implementing the Linear Allocator
Now, let's turn our design into code. The most critical and subtle part of the implementation is the allocate() method, which must correctly handle memory alignment.
As we discussed in a previous lesson, some data types must be stored at memory addresses that are multiples of their size to ensure correct and performant CPU access. Our allocator must respect this.
Here is a visual representation of an allocation that requires padding to meet alignment requirements.

The articles below provide excellent, complete implementations of a C++ arena allocator. We will use them as a guide to build our own.
High Performance Memory Management: Arena Allocators
First, let's study the implementation from the 'High Performance Memory Management: Arena Allocators' article we looked at briefly in the last lesson. It provides a clean, modern C++ implementation.
Please read the section with the C++ code for the Arena class. Focus on how the allocate method is implemented. It uses std::align, a standard library function designed specifically for this purpose. Understand its parameters and what it does.
Hands-on Practice: Implementing a Basic Memory Allocator
For a second perspective, this article provides a very similar implementation but writes its own helper function for alignment instead of using std::align. Seeing both approaches is instructive.
Review the code for the ArenaAllocator class in this article. Notice how the align_pointer helper function manually calculates the next aligned address. The logic inside allocate is then very similar to the previous example.
The Complete Class
Drawing inspiration from these resources, let's assemble our LinearAllocator class. We will use the std::align approach as it's a standard C++ tool.
#pragma once // Prevents the header from being included multiple times
#include <cstddef> // For std::size_t, std::byte
#include <memory> // For std::align
#include <new> // For std::bad_alloc
class LinearAllocator {
public:
// Constructor: allocates the memory block.
explicit LinearAllocator(std::size_t size)
: buffer_start_(static_cast<std::byte*>(::operator new(size))),
capacity_(size),
current_offset_(0) {
if (!buffer_start_) {
throw std::bad_alloc();
}
}
// Destructor: frees the memory block.
~LinearAllocator() {
::operator delete(buffer_start_);
}
// Allocate memory of `size` bytes with specified `alignment`.
void* allocate(std::size_t size, std::size_t alignment) {
// Get a pointer to the current allocation spot.
void* current_ptr = buffer_start_ + current_offset_;
// Calculate the remaining space in the buffer.
std::size_t space = capacity_ - current_offset_;
// Use std::align to find the next aligned address.
// `align` will modify current_ptr and space.
if (std::align(alignment, size, current_ptr, space) == nullptr) {
// Not enough space for an aligned allocation of this size.
throw std::bad_alloc();
}
// `current_ptr` now points to the aligned address.
// We calculate the new offset based on this aligned address plus the requested size.
current_offset_ = static_cast<std::byte*>(current_ptr) - buffer_start_ + size;
return current_ptr;
}
// Reset the allocator to reuse the memory block from the start.
void reset() {
current_offset_ = 0;
}
// For simplicity and safety, linear allocators managing a large resource
// should not be copied or moved.
LinearAllocator(const LinearAllocator&) = delete;
LinearAllocator& operator=(const LinearAllocator&) = delete;
private:
std::byte* buffer_start_; // Start of the allocated memory block
std::size_t capacity_; // Total size of the block
std::size_t current_offset_; // Current allocation offset
};
Let's break down the allocate function, as it's the most complex part:
void* current_ptr = buffer_start_ + current_offset_;We get a pointer to our current position.std::size_t space = capacity_ - current_offset_;We calculate how much space is left.std::align(alignment, size, current_ptr, space): This is the magic.- You give it the
alignmentyou need and thesizeyou want to allocate. - You pass
current_ptrandspaceby reference. std::alignwill figure out the next address that satisfiesalignment. It modifiescurrent_ptrto point to this new aligned address. It also modifiesspaceto tell you how much space is left after this new aligned address.- It returns
nullptrif it's impossible to fit an aligned block ofsizebytes in the remainingspace.
- You give it the
current_offset_ = ...: Ifstd::alignsucceeds, we update our maincurrent_offset_to be past the newly allocated block, ready for the next call toallocate.
Test your understanding!
Imagine you have a LinearAllocator with 100 bytes. The current_offset_ is at 10. You call allocate(8, 8). The pointer at offset 10 is not 8-byte aligned. Let's say std::align finds the next 8-byte aligned address at offset 16.
- What address will
allocatereturn (as an offset)? - What will the new
current_offset_be after the allocation is complete?
Show answer
allocatewill return a pointer corresponding to offset 16. This is the first available address that satisfies the 8-byte alignment requirement. The memory from offset 10 to 15 is now "padding" and is wasted.- The new
current_offset_will be 24. This is calculated as the starting aligned address (16) plus the size of the allocation (8).
Using the Allocator
Now we can combine our shiny new LinearAllocator with the placement new knowledge from our last lesson.
#include <iostream>
#include "LinearAllocator.h" // The class we just wrote
struct Vector3 {
float x, y, z;
};
struct GameObject {
int id;
Vector3 position;
const char* name;
GameObject(int i, Vector3 pos, const char* n)
: id(i), position(pos), name(n) {
std::cout << "Constructed GameObject '" << name << "' at " << this << std::endl;
}
~GameObject() {
std::cout << "Destructed GameObject '" << name << "'" << std::endl;
}
};
int main() {
// Create an allocator for the scope of our "game frame".
LinearAllocator frame_allocator(1024); // 1KB arena
try {
// --- Frame 1 ---
std::cout << "--- Start of Frame 1 ---" << std::endl;
// Allocate raw memory for the first object
void* mem1 = frame_allocator.allocate(sizeof(GameObject), alignof(GameObject));
// Construct the object using placement new
GameObject* obj1 = new (mem1) GameObject(1, {10, 20, 30}, "Player");
// Allocate and construct a second object
void* mem2 = frame_allocator.allocate(sizeof(GameObject), alignof(GameObject));
GameObject* obj2 = new (mem2) GameObject(2, {50, 10, -20}, "Enemy");
// IMPORTANT: We must manually call destructors for objects created with placement new
obj1->~GameObject();
obj2->~GameObject();
std::cout << "--- End of Frame 1 ---" << std::endl;
frame_allocator.reset(); // All memory from frame 1 is now invalid.
// --- Frame 2 ---
std::cout << "\n--- Start of Frame 2 ---" << std::endl;
// We can now reuse the allocator's memory from the beginning
void* mem3 = frame_allocator.allocate(sizeof(GameObject), alignof(GameObject));
GameObject* obj3 = new (mem3) GameObject(3, {0, 0, 0}, "Camera");
obj3->~GameObject();
} catch (const std::bad_alloc& e) {
std::cerr << "Allocator ran out of memory!" << std::endl;
}
return 0;
}
This example perfectly simulates how our telemetry system will work: allocate many events during a frame, process them, and then reset the allocator for the next frame.
Conclusion
Congratulations! You have now implemented your first and arguably most important custom memory allocator. While simple, the linear allocator is a cornerstone of high-performance C++ programming, found in game engines, trading systems, and compilers—anywhere predictable allocation patterns can be exploited for maximum speed.
Key Takeaways:
- A linear allocator is implemented with a memory buffer, a capacity, and a current offset.
- The
allocateoperation is a "bump pointer" operation: find the next correctly aligned address and move the offset past the newly allocated block. std::alignis the standard library tool for handling alignment calculations correctly and safely.- The
resetoperation is O(1)—it simply moves the offset back to zero, making mass deallocation instantaneous. - This allocator, combined with
placement new, gives us full control over memory layout and lifetime, which is essential for low-overhead data capture.
In our next lesson, we will address a limitation of our current approach. We've been allocating and constructing single objects. But what if we need an array of objects? How do we correctly construct and—more importantly—destruct every object in that array? We'll tackle this challenge by learning how to manage memory for arrays of objects with proper construction and destruction.