I Reject Commits That Use the Heap
A game developer explains why he rejects code commits that rely on heap allocation in performance-critical game engines, and demonstrates C++ patterns like fixed-size containers, CRTP, std::variant, and object pools as alternatives.
I want to share my experience developing large game projects in C++, where performance and stability aren't just nice bonuses — they're absolutely natural requirements. Over years of working on engines and games, I've come to understand that the approach to memory management profoundly affects the entire project. Unlike many applications, games — especially large ones — often run for hours without interruption and must maintain stable frame rates and responsiveness. When FPS drops or freezes happen in front of hundreds of thousands of players, nobody can help you — the damage is done, and Steam reviews about the developers' incompetence are already flooding in.

My team once finished working on a fairly interesting project that we spent over two years porting to PlayStation. The engine was old, large, and powerful, but its memory management was oriented toward PCs from the late 2000s. What struck me was how heavily a large portion of the codebase depended on dynamic memory at runtime. On constrained hardware (not everyone has a PS5 Pro) and under strict console certification requirements, such decisions quickly become problematic.
In console development (I won't even mention mobile — the game doesn't fit in eight gigabytes of memory) with limited resources, an architecture with frequent allocations isn't just inefficient — it becomes a real threat to project stability. Every heap allocation carries overhead: additional milliseconds of delay per frame, risk of severe memory fragmentation, and unpredictable behavior during long gaming sessions. After two hours of play, constant heap operations literally "burn" half the frame budget.
Dynamic Memory Is a Problem in Game Development
In games and game engines, especially on consoles and mobile devices, memory management must be maximally predictable. This means:
- No unexpected delays from heap fragmentation
- No risk of FPS drops due to memory exhaustion during a fight or important scene
- No gradual performance degradation during gameplay
- No allocation errors that could ruin a match or cause crashes
Unlike desktop applications where a user can "restart" the program and continue — a game must run stably on constrained hardware with a fixed memory budget. When memory runs out — and it physically does run out — the game crashes. On consoles and mobile devices, this isn't a theoretical threat but a practical reality that directly affects the experience of thousands of players.
Console memory budgets:
| Console | Total Memory | Available for Game | Architecture Notes |
|---|---|---|---|
| PlayStation 5 | 16 GB GDDR6 | 12.5-13 GB | Unified architecture |
| Xbox Series X | 16 GB GDDR6 | ~11.5 GB | 10 GB high-speed + 6 GB standard |
| Xbox Series S | 10 GB GDDR6 | ~7 GB | 8 GB high-speed + 2 GB standard |
The Hidden Costs of Dynamic Memory
My measurements show that heap allocation time degrades 2-5x on Xbox and 2-3x on PlayStation 5 during extended play sessions (around three hours), directly impacting game performance. On mobile, such long sessions are rare, but fragmentation can "eat" up to 30% of available memory during longer (30+ minute) gaming sessions. For platforms with limited resources, this means not only FPS drops but actual out-of-memory crashes.
It may seem like a minor detail, but different malloc implementations add 24-64 bytes of overhead per allocation for bookkeeping. In a game where thousands of small allocations happen per frame — and I'm not exaggerating, thousands per frame (for example, when creating objects or effects) — this overhead alone consumes a significant chunk of memory.
Allocator overhead comparison:
| Allocator | Metadata Overhead | Notes |
|---|---|---|
| glibc malloc | 16-24 bytes | Stores block size + flags + pointers/links in free lists |
| jemalloc | Separate from blocks, but overhead still exists — several bytes per block plus additional structures | Overhead depends on allocation size and class |
| TCMalloc | 32+ bytes/class | Additional costs for thread cache data, size-class management; higher overhead for small allocations |
| Windows/Xbox | 48+ (debug) / 30+ bytes (release) | Depends on OS version, mode (debug/release) and architecture |
| PlayStation | Min 12 bytes (size + truncated next-block pointer + flags); 80+ bytes for debug builds | No exact figure; depends on SDK version |
Heap-Free Code in Practice
Like many of my colleagues in game development, I believe that modern C++ features have become too "heavy" or unnecessary for game development — but all that syntactic sugar does let you write physically less code. You can successfully use lambdas, RAII, static polymorphism, and even not-yet-fully-explored C++23 features for game development. The key is not to reject modern tools but to apply them wisely, understanding our systems' limitations and using only those language features that don't violate performance and predictability requirements.
At some point, the team realized we needed analogs of familiar STL containers but with fixed sizes. For example, our gtl::vector<T, N> works exactly like std::vector<T> but can hold a maximum of N elements. All memory is allocated at object creation time, not dynamically when adding elements. This approach offers numerous benefits: container size is known at compile time, enabling static analysis of memory consumption; add and remove operations execute in predictable time since they don't require calls to the memory management system.
The standard std::function in C++ uses dynamic memory allocation for storing large objects, which is completely unacceptable for games when every other handler wraps a lambda in a functor. Several libraries solve this problem — for example, Don Clugston's FastDelegate library, written twenty years ago but still relevant, or ETL's etl::function<Signature, StorageSize> implementation that uses an in-place buffer for storing the function object. This lets you use all the benefits of functional programming — lambdas, functors, function pointers — without the risk of uncontrolled memory allocation. We define the maximum size of the function object ourselves, and if it exceeds the limit, the compiler simply produces an error.
// <<<< std::vector<int>
gtl::vector<int, 64> _unit_options;
// <<<< std::function<void()>
gtl::function<void(), 32> _unit_death_cb;If a programmer tries to add an element to an already full container or store a function object that's too large, the code simply won't compile. This is far better than getting runtime errors. This approach catches potential problems before the program reaches the player, and static code analysis becomes more effective since the compiler can precisely determine maximum memory consumption.
Adding CRTP to the Mix
In traditional C++ OOP, we often use virtual functions for polymorphism — a base class with virtual methods and several derived classes. The compiler creates a virtual function table (vtable), and at each method call, the program first consults this table to determine which function to invoke.
This mechanism creates several problems. While they're not as critical as they were ten to fifteen years ago, the problems haven't disappeared — processors just got faster. First, every virtual function call requires an additional memory access to retrieve the actual function address from the table. Second, polymorphic objects typically require dynamic memory allocation since object size is unknown at compile time. Third, virtual destructors complicate memory management and can lead to unpredictable behavior.
CRTP (Curiously Recurring Template Pattern) is a programming pattern where a class inherits from a template base class, passing itself as the template parameter. It sounds complex, but in practice it's an elegant solution: class Derived : public Base<Derived>. The base class can call derived class methods through static_cast, with all calls resolved at compile time. It's ideal when we know all possible types at compile time and want to eliminate virtual function calls.
template <typename Derived>
class GameObject {
public:
void update() {
static_cast<Derived*>(this)->updateImpl();
}
void render() {
static_cast<Derived*>(this)->renderImpl();
}
};
class Player : public GameObject<Player> {
public:
void updateImpl() {
// Player update logic
}
void renderImpl() {
// Player rendering logic
}
};Static Polymorphism with std::variant
An alternative approach uses runtime polymorphism via std::variant, where the selection of a specific method implementation happens at compile time rather than at runtime. The compiler knows in advance which function to call and generates a direct call without intermediate lookups through tables or pointers. This completely eliminates runtime overhead associated with polymorphism. Everything runs as fast as if you were calling the function directly, while the code remains flexible and extensible.
struct ButtonEvent {
int button_id;
void process() { /* Button press handling */ }
};
struct TimerEvent {
int timer_id;
void process() { /* Timer handling */ }
};
struct NetworkEvent {
std::string message;
void process() { /* Network event handling */ }
};
using Event = std::variant<ButtonEvent, TimerEvent, NetworkEvent>;
struct EventProcessor {
template<typename T>
void operator()(T& event) const {
event.process();
}
};
Event e1 = ButtonEvent{42};
Event e2 = TimerEvent{7};
Event e3 = NetworkEvent{"Hello"};
std::visit(EventProcessor{}, e1);
std::visit(EventProcessor{}, e2);
std::visit(EventProcessor{}, e3);Placement New and Object Pools
Games often need to rapidly create and destroy many objects — projectiles, effects, particles, ropes, and decals. Instead of using regular dynamic memory, we use static pools that allow object recycling without the overhead of new/delete and memory fragmentation — especially important on consoles and mobile devices.
gtl::pool<Projectile, 64> projectile_pool;
auto* proj = projectile_pool.allocate();
proj->velocity = . . .;
proj->damage = . . .;
projectile_pool.deallocate(proj);Sometimes it's important to precisely control the order and timing of object initialization. This approach — similar to a pool but not quite — can be used for startup allocations to create unique game-level objects: configs, systems, render managers, audio managers, etc.
alignas(GameConfig) char _gameConfigStorage[sizeof(GameConfig)];
GameConfig* config = new(_gameConfigStorage) GameConfig();
config->load_from_file({. . .});
// After use, manually call the destructor
// or don't call it at all — this object lives for the entire game
config->~GameConfig();
template<typename T>
class GameResource {
alignas(T) mutable uint8_t _data[sizeof(T)];
mutable T* _instance = nullptr;
public:
template<typename... Args>
T& init(Args&&... args) const {
if (_instance) {
_instance->~T();
}
_instance = new (_data) T(std::forward<Args>(args)...);
return *_instance;
}
void destroy() const {
if (_instance) {
_instance->~T();
_instance = nullptr;
}
}
T& ref() const {
assert(_instance);
return *_instance;
}
};
GameResource<GameConfig> g_config;
void Game::Init() {
. . .
g_config.init({100});
. . .
}The Certification Wake-Up Call
One of the main reasons we suddenly started scrutinizing memory usage so closely was complaints from new players and — unexpectedly — a certification rejection, which is never pleasant and raises reasonable questions from company leadership.
During the certification phase, the vendor's testing lab documented low FPS, memory corruption, and performance degradation — and we thought they were just playing the build — and refused to approve the game for publication. This was an unpleasant surprise for the entire team, and we had to rearchitect our memory usage, implement fixed allocators, and nearly eliminate all dynamic memory allocations. We didn't eliminate all of them, of course, but now the main thread has hundreds of allocations per frame — down from an order of magnitude more.

Why Doesn't the Hot Dog Vendor Eat His Own Hot Dogs?
In my case — the hot dog vendor does eat his own hot dogs and is forced to feed them to the entire team, but the mice cry and complain. When using C++, you shouldn't "accept" heap usage as a given — you can and should architect your system to avoid dynamic memory allocation at runtime. C++ still lets you enjoy the benefits of modern standards — lambdas, RAII, templates, and even new C++23 features — while maintaining predictable system behavior.
The right architectural approach lets you create reliable code without sacrificing performance. Working without the heap doesn't limit capabilities — on the contrary, it forces you to write cleaner, more predictable, and more resilient code. In game development, predictability is very often more important than flexibility, so when designing your system, you need to think ahead about where and how memory will be used.
Despite the obvious technical advantages, performance graphs, internal presentations, and mentoring — implementing everything described above in real projects often meets team resistance. Many developers are accustomed to classical OOP with virtual functions, inheritance, and syntactic sugar that seems more intuitive and understandable. This approach requires a different way of thinking about code — you need to think about types, use cases, template magic, and variant unions. And yes, the syntax becomes more complex, which can initially intimidate and repel.
Unfortunately, practice shows that at the first sign of difficulty or deadline pressure, teams quickly revert to familiar habits. "Let's just make a regular interface with virtual methods — it's faster and everyone understands it" — a reaction the author has seen many times under time pressure.
This approach is especially difficult to adopt in teams with high turnover or outsourced developers who aren't willing to invest time learning the studio's code style. The result is a mix of styles that creates technical inconsistency and generally complicates future maintenance.
I'm curious to hear from Habr readers and C++ developers — who among you builds projects without the heap? What techniques and strategies helped you maintain code readability without sacrificing modern language features? Have you had to convince your team to adopt such practices?