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.

  1. 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.
  2. 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.
  3. 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 new or malloc, 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 TransformComponent data 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, and HealthComponent.
  • 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 PhysicsSystem only reads RigidBody and Transform components.
  • A RenderSystem only reads Mesh and Transform components. 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.