Tag: RAII

  • C++ Game Memory: Ownership, Locality and Evidence Before Pools

    C++ Game Memory: Ownership, Locality and Evidence Before Pools

    C++ does not impose a language-wide tracing garbage collector. That does not make memory automatic or free: a game can still leak, double-delete, retain shared ownership indefinitely, fragment heaps or spend too much time allocating and traversing scattered data. The first optimisation is a clear lifetime model; the second is measurement.

    Modern C++ uses scope, value semantics and Resource Acquisition Is Initialization (RAII) to pair resource lifetime with object lifetime. The C++ Core Guidelines recommend expressing ownership through resource handles and avoiding naked new and delete in application code (C++ Core Guidelines — Resource management).

    Article map for C++ Game Memory: Ownership, Locality and Evidence Before Pools, covering Draw the ownership graph, Prefer values and contiguous storage, Measure allocations by context and related review points.
    Article map: Draw the ownership graph; Prefer values and contiguous storage; Measure allocations by context; Add arenas and pools only for a proven pattern.

    Draw the ownership graph

    For each important object family, answer:

    • who creates it;
    • who owns its lifetime;
    • who may observe it without ownership;
    • when it is destroyed;
    • whether identity must remain stable; and
    • which thread may mutate it.

    Prefer a single clear owner. A scene can own entities by value in a container; a subsystem can own an implementation through std::unique_ptr; a scoped handle can own a file, socket, GPU resource or lock. Non-owning references should be visibly non-owning and valid for a documented lifetime.

    Use std::shared_ptr when ownership is genuinely shared and the destruction point cannot be represented more simply. It adds a control block, reference-count operations and the possibility of cycles. Passing shared_ptr everywhere does not make lifetime safe; it makes ownership harder to see. Use std::weak_ptr only as part of a deliberately shared model, not to repair an unclear graph.

    Prefer values and contiguous storage

    Game loops often process many similar objects. Contiguous storage can improve locality and reduce per-object allocation. A straightforward bullet store might begin as:

    struct Bullet {
        Vec2 position;
        Vec2 velocity;
        float remaining_seconds;
        EntityId owner;
    };
    
    std::vector<Bullet> bullets;
    bullets.reserve(max_simultaneous_bullets);

    reserve can avoid repeated capacity growth when a credible maximum is known. It is not a free optimisation: reserved capacity consumes address space and memory, and a later reallocation invalidates pointers, references and iterators to elements. Use stable identifiers or a container designed for the required stability instead of retaining accidental addresses.

    Array-of-structures, structure-of-arrays and hybrid layouts suit different access patterns. If one update reads position and velocity for every live particle, storing hot fields tightly may help. If gameplay frequently needs a complete object, splitting every field can add complexity. Use hardware counters and representative profiles rather than adopting a fashionable layout by default.

    Keep cold metadata, editor names and rarely used debug data away from hot loops where practical. Remove padding only with evidence; packed structures can create misaligned access and platform problems. Check sizeof, alignment and actual cache-miss behaviour on supported targets.

    Measure allocations by context

    Record allocation count, allocated bytes, high-percentile allocation time, peak resident memory and fragmentation indicators during loading, gameplay, transitions and shutdown. A total count without call stacks or lifetime groups rarely identifies the cause.

    Allocations during loading may be harmless while the same work inside a frame-critical loop causes spikes. Conversely, a “zero allocations per frame” target can encourage complicated pools that retain excessive memory. Define budgets per subsystem and phase.

    Use the engine or platform memory profiler plus instrumented allocators where available. Capture the exact build, optimisation level, device, scene and duration. Debug allocators change layout and timing, so correlate tool results with a representative release build.

    Decision path for C++ Game Memory: Ownership, Locality and Evidence Before Pools, covering Prefer values and contiguous storage, Measure allocations by context, Add arenas and pools only for a proven pattern and related…
    Decision path: Prefer values and contiguous storage; Measure allocations by context; Add arenas and pools only for a proven pattern; Use tools to find correctness defects.

    Add arenas and pools only for a proven pattern

    An arena allocates from a larger region and releases many allocations together. It works well when contained objects share a lifetime, such as temporary data for one frame or level load. It is dangerous when references escape the arena's reset boundary.

    A fixed-size pool can bound cost and provide stable slots for many same-sized objects. It also needs generation counters or another defence against stale handles, explicit exhaustion behaviour, alignment, constructor and destructor handling, thread rules and telemetry.

    Before adding one, state the hypothesis:

    The projectile subsystem performs many same-sized heap allocations during combat, contributing a measured frame-time spike. A bounded pool of the observed high-water mark plus agreed headroom should reduce allocator time without unacceptable retained memory.

    Then compare before and after. Include worst-case spawn bursts, pool exhaustion, scene reload and long sessions. If the standard container already meets the budget, keep the simpler design.

    std::pmr resources can provide allocator strategies through standard interfaces, but they do not decide lifetime or thread safety for the project. A monotonic resource is appropriate only where all associated allocations can be discarded together.

    Use tools to find correctness defects

    Undefined behaviour can appear as a performance anomaly. Compile dedicated test builds with warnings and sanitizers. Clang's AddressSanitizer detects classes of out-of-bounds access, use-after-free, double-free and related errors; LeakSanitizer detects leaked allocations on supported platforms (Clang — AddressSanitizer, Clang — LeakSanitizer).

    Sanitizer builds have runtime and memory overhead and are not production binaries. Run unit tests, asset pipelines, headless simulations and representative play sessions under them. Also use static analysis, assertions, fuzzing for parsers and platform-specific tools. One clean run does not prove the absence of lifetime bugs.

    Review common failure modes

    • returning a pointer or view into an object whose owner has ended;
    • retaining vector element addresses across growth or erase;
    • creating shared_ptr cycles;
    • pooling an object without resetting subscriptions, timers or state;
    • resetting an arena while jobs still read it;
    • assuming an allocator is thread-safe;
    • measuring only average memory rather than transition peaks; and
    • optimising memory layout before confirming the frame is memory-bound.

    Make lifetime violations fail early in development. Use stable handles with generation checks where objects can be removed and slots reused. Make shutdown and scene unload part of automated tests, not an afterthought.

    For help planning a C++ profiling experiment or runtime architecture review, see Ozlin Info's game-development services or contact Ozlin Info.

    Related reading: Game state management through explicit ownership.


    Control and evidence map for C++ Game Memory: Ownership, Locality and Evidence Before Pools, covering Add arenas and pools only for a proven pattern, Use tools to find correctness defects, Review common failure modes an…
    Control and evidence map: Add arenas and pools only for a proven pattern; Use tools to find correctness defects; Review common failure modes; General-information disclaimer.

    General-information disclaimer

    This article provides general technical information. Memory behaviour depends on compiler, standard library, engine, allocator, workload and hardware; no performance or defect-free outcome is guaranteed.

    AI-assistance disclosure

    AI tools assisted with source discovery, outlining, example drafting and copyediting. A human reviewer must compile, test, profile and review lifetimes against the actual codebase and supported targets before publication or implementation.

    Practical checklist for C++ Game Memory: Ownership, Locality and Evidence Before Pools, covering Review common failure modes, General-information disclaimer, AI-assistance disclosure and related review points.
    Practical checklist: Review common failure modes; General-information disclaimer; AI-assistance disclosure; Primary sources checked.

    Primary sources checked

    Source access date: 29 August 2026.