Inheritance vs Composition Performance in Game Dev
Deep class inheritance and composition patterns represent two fundamentally different architectural paradigms in gameplay programming, each with starkly contrasting impacts on hardware performance. While deep inheritance relies on polymorphic object-oriented hierarchies that introduce significant CPU cache misses, virtual function overhead, and memory bloat, composition patterns—particularly data-oriented and Entity Component System (ECS) architectures—prioritize contiguous memory layouts, high cache locality, and predictable instruction pipelines. This article analyzes the primary performance bottlenecks associated with deep inheritance hierarchies in game development and contrasts them with the computational and memory efficiencies offered by composition.
Dynamic Dispatch and Vtable Overhead
Deep inheritance hierarchies rely heavily on runtime polymorphism
implemented via virtual method tables (vtables). When an engine calls an
overridden function on an object situated deep within an inheritance
tree (e.g.,
Entity -> Actor -> Pawn -> Character -> PlayerCharacter),
the CPU must execute an indirect call.
- Pointer Indirection: The CPU reads the object's instance to find its vtable pointer, dereferences the pointer to access the vtable, and then indexes into the table to retrieve the target function's address before executing the instruction.
- Branch Misprediction: Because gameplay systems
often iterate over mixed collections of base pointers (such as arrays of
Actor*), the target function changes frequently between iterations. Modern CPU branch predictors struggle to anticipate these dynamic addresses, leading to pipeline flushes that cost anywhere from 10 to 20 CPU cycles per mispredicted call. - Inlining Impediment: Compilers cannot inline virtual functions whose targets are determined at runtime. Inlining eliminates call overhead and opens doors for further compiler optimizations such as constant folding and loop vectorization. Deep inheritance structures severely limit the compiler’s ability to perform these optimizations.
In contrast, composition patterns that utilize flat component arrays or static polymorphism (such as CRTP or templates) invoke non-virtual, direct function calls. This predictability allows compilers to aggressively inline critical gameplay logic and keeps the CPU instruction pipeline full.
Cache Locality and Memory Access Patterns
Modern game performance is predominantly constrained by memory latency rather than raw compute cycles. Fetching data from main memory (RAM) can take up to 200–300 CPU cycles, while fetching from the L1 cache takes approximately 4–5 cycles.
Deep inheritance structures inherently lead to an Object-Oriented Design (OOD) pattern known as Array of Structures (AoS). These objects:
- Scatter Across the Heap: Instances are typically
allocated dynamically via
newormalloc, leading to heap fragmentation. Iterating over an array of object pointers causes the memory controller to pull disparate, non-contiguous cache lines from RAM. - Pollute Cache Lines: A deep hierarchy causes individual objects to grow large because they inherit all fields defined by every ancestor in the chain. When a system iterates over entities to update a single property (such as position), it loads an entire 64-byte cache line containing irrelevant inherited variables (like health, faction IDs, and animation states), evicting potentially useful data from L1/L2 caches.
Composition-based patterns, particularly data-oriented designs, decouple data from identity using contiguous memory arrays:
- Structure of Arrays (SoA): Systems store like
components contiguously in memory (e.g., all
TransformComponentdata in a single array). - High Spatial Locality: When a movement system processes updates, every byte pulled into a 64-byte CPU cache line is strictly transformation data used in that frame's calculation, achieving near 100% cache utilization.
- Hardware Pre-fetching: Sequential memory reads allow the CPU hardware prefetcher to anticipate upcoming data, pre-loading it into the L1/L2 cache ahead of execution and effectively hiding memory access latency.
Object Bloat and Memory Footprint
Deep inheritance imposes an "all-or-nothing" memory cost. If a base
class Actor contains transform data, replication handles,
input buffers, and collision flags, every derived class inherits that
entire memory footprint, regardless of whether it requires those
features.
- A static environmental object (like a breakable crate) derived from a heavy base class wastes dozens or hundreds of bytes of unused memory per instance.
- Over thousands of active gameplay entities, this accumulated dead data bloats the game’s overall memory footprint and degrades overall system throughput.
Composition patterns allocate state modularly. Entities function merely as lightweight IDs (often 32-bit or 64-bit integers), and components are attached conditionally:
- An interactive static chest only receives a
TransformComponent,MeshComponent, andHealthComponent. - It pays zero memory or execution cost for pathfinding, AI states, or player inputs.
- This granular memory allocation minimizes waste and increases the total number of simultaneous entities an engine can simulate within tight frame budgets (e.g., 16.6ms for 60 FPS or 8.3ms for 120 FPS).
Architectural Scalability and Parallelism
Deep inheritance creates tight temporal and spatial coupling. Methods deep in the tree frequently read and modify state scattered throughout their parent classes. This design pattern poses significant barriers to multi-threading:
- Detecting data dependencies between sibling or parent classes is difficult, making it risky to process object updates across multiple CPU cores without introducing race conditions.
- Synchronization primitives (such as mutexes or atomic locks) introduced to make deep classes thread-safe introduce immense latency and lock contention.
Composition organizes data by domain. Because components represent discrete chunks of isolated data, dependencies are explicit:
- A
PhysicsSystemonly readsRigidBodyandTransformcomponents. - A
RenderSystemonly readsMeshandTransformcomponents. This strict separation allows modern job-based schedulers to execute systems in parallel across worker threads with minimal data contention and without requiring complex lock mechanisms.