Torna al Blog
2024-11-2011 min readEnglish

Nota Asimmetrica: Questo saggio è stato redatto in lingua inglese ed è presentato nel testo autoriale originario.

Zero-Allocation Game Loops: Designing for Strict Frame Budgets

Techniques to eliminate runtime GC hiccups and dynamic heap allocations in continuous interactive simulations.

#Game Dev#Systems#Memory#Simulation

In a real-time game simulation running at 60 frames per second, each frame has an uncompromising temporal deadline: 16.66 milliseconds. At 120 or 144 Hz display refresh rates, that window collapses to 8.33 ms or 6.94 ms.

Within this budget, the application must process raw input, advance spatial physics integrations, compute artificial intelligence steering, resolve collision intersections, prepare render queues, and dispatch draw calls to the GPU.

If a garbage collector or heap allocator triggers a non-deterministic 15-millisecond stop-the-world compaction pause, an entire frame is dropped. To the human eye, this manifests as jarring hitching—a perceptible violation of physical continuity.

The solution is structural: zero runtime heap allocations.

The Philosophy of Arena and Pre-allocated Ring Buffers

Dynamic memory allocation (malloc, free, new, or implicit GC object instantiations) is inherently non-deterministic. Operating system allocators traverse free-lists, lock mutexes across threads, and trigger kernel-level page faults.

In a deterministic engine architecture, all memory is acquired during engine bootstrap:

  1. Static Global Arenas: Sized upfront to the worst-case system capacity.
  2. Per-Frame Linear Bump Allocators: Scratch memory reset with a pointer rewind at the end of each frame (arena.offset = 0).
  3. Circular Ring Buffers: Fixed-capacity ring buffers for inter-thread message passing and sensory event telemetry.
typedef struct {
    uint8_t* buffer;
    size_t capacity;
    size_t offset;
} LinearFrameArena;

void* frame_alloc(LinearFrameArena* arena, size_t size, size_t alignment) {
    size_t current_ptr = (size_t)(arena->buffer + arena->offset);
    size_t aligned_ptr = (current_ptr + (alignment - 1)) & ~(alignment - 1);
    size_t new_offset = (aligned_ptr - (size_t)arena->buffer) + size;

    if (new_offset > arena->capacity) {
        // Hard panic or fatal assertion during development: frame budget exceeded
        return NULL;
    }

    arena->offset = new_offset;
    return (void*)aligned_ptr;
}

void frame_reset(LinearFrameArena* arena) {
    // Zero deallocation overhead: instantaneous O(1) rewind
    arena->offset = 0;
}

Because allocating is simply advancing an integer index (arena->offset += size) and resetting is setting that index back to zero, allocation latency drops from hundreds of nanoseconds to approximately two CPU clock cycles.

Structure of Arrays (SoA) vs Array of Structures (AoS)

Traditional object-oriented design groups data by entity:

// Array of Structures (AoS) - Cache Inefficient for Batch Physics
struct Particle {
    Vector3 position;     // 12 bytes
    Vector3 velocity;     // 12 bytes
    Color color;          // 4 bytes
    float life;           // 4 bytes
    uint32_t flags;       // 4 bytes
}; // Total: 36 bytes (padded to 40)

Particle particles[10000];

When integrating particle positions (pos += vel * dt), the CPU loads 40 bytes per particle into L1 cache, discarding the color, life, and flags fields. 40% of the cache line is pure unutilized bandwidth.

By refactoring into Structure of Arrays (SoA):

// Structure of Arrays (SoA) - SIMD & Cache Line Optimal
struct ParticleSystem {
    float pos_x[MAX_PARTICLES];
    float pos_y[MAX_PARTICLES];
    float pos_z[MAX_PARTICLES];
    float vel_x[MAX_PARTICLES];
    float vel_y[MAX_PARTICLES];
    float vel_z[MAX_PARTICLES];
    // Color and life reside in separate streams
};

Consecutive floats packed side-by-side allow modern vector registers (AVX-512 or NEON) to update eight or sixteen particles in a single single-instruction multiple-data (SIMD) instruction.

Continuous Integration & Frame-Budget Enforcement

To protect frame-rate consistency across commits, our automated test pipelines hook into OS allocation symbols:

Final Reflection

Building games and simulations without garbage collection is not an aesthetic constraint; it is a discipline of respect for the machine and the user’s perception of time. When memory layout reflects hardware physics, speed ceases to be a feature—it becomes an invariant.

Davide BertoniIngegnere Informatico & Costruttore di Sistemi • Politecnico di Milano