DVDevournal

§3 Performance

Profiling Your Game: Finding the 3% of Code Eating 90% of Your Frame

Most performance work is wasted, because it's aimed at code that was never the problem. The frame is slow, so you rewrite the thing that feels slow, spend three days on it, and the frame time barely moves. Profiling replaces that guessing with a number, and the number is almost always more lopsided than you expect.

The frame budget is the only target that matters

Sixty frames per second means each frame gets 16.67 milliseconds — for everything: input, simulation, animation, culling, draw calls, the lot. Thirty fps gives you 33.3 ms. That budget is your ruler. "Is this function slow?" is unanswerable; "does this function fit the budget?" has a yes-or-no answer. Before you optimise a single line, know your target frame time and how far over it you are.

Amdahl, roughly: speeding up code that's 5% of the frame can, at absolute best, make the frame 5% faster. Find the big slice first. A 2× win on the hot path beats a 100× win on a cold one.

Measure the whole frame before you zoom in

Start coarse. Wrap the top-level phases in timers and print a per-frame breakdown. You don't need a fancy tool for this first pass — a scoped timer that accumulates into named buckets is enough to point you at the right continent before you go looking for the right street.

struct ScopeTimer {
    const char* name;
    std::chrono::high_resolution_clock::time_point start;
    ScopeTimer(const char* n)
        : name(n), start(Clock::now()) {}
    ~ScopeTimer() {
        auto us = duration_cast<microseconds>(
                    Clock::now() - start).count();
        Profiler::record(name, us);
    }
};

// usage
{ ScopeTimer t("physics");   stepPhysics(dt); }
{ ScopeTimer t("render");    renderScene();  }

Run it, look at the buckets. Nine times out of ten one phase dwarfs the rest. That's your continent. Now you can afford to pull out the real tool.

Read the flame graph, don't skim it

A sampling profiler — perf, Instruments, Superluminal, VTune, Tracy — interrupts your program hundreds of times a second and records the call stack each time. Aggregate those samples and you get a flame graph: stacked bars where width is time. It's the single most useful performance picture there is, and most people misread it.

The "3% of code, 90% of frame" in the title is not hyperbole. On a real project I once found a single std::map lookup, called once per particle per frame, taking 70% of the simulation because the map hashed a string key every time. Twelve lines changed to an integer key. The frame dropped from 22 ms to 9 ms. No amount of staring at the code would have revealed it — only the sampler did.

Change one thing, then prove it

This is the step people skip and the reason optimisation gets a bad name. After a fix, re-measure the exact same scene under the exact same conditions and compare frame times. If the number didn't move, revert the change — even if it "should" be faster. Speculative optimisation that doesn't show up in the profile is just added complexity and new bugs.

  1. Capture a baseline on a fixed, repeatable scene.
  2. Read the flame graph; pick the single widest self-time bar.
  3. Make one focused change to that function.
  4. Re-capture the same scene; compare against the baseline.
  5. Keep it only if the frame time actually dropped. Then repeat from step 2.

A few reliable hot spots in games

Once you've done this a dozen times, patterns emerge. Per-frame heap allocations are a classic — pool them. Cache-hostile iteration over pointer-chasing structures shows up constantly, which is exactly why the ECS layout keeps components in packed arrays. Redundant state changes on the GPU (binding the same shader or texture over and over) inflate the render bucket. And doing work every frame that only changes occasionally — rebuilding a navigation mesh, re-sorting a static list — is free performance the moment you cache it.

But don't take my list and start "fixing" these blind. That's just guessing with extra steps. Measure your frame, read your flame graph, and let the profiler tell you which of these — if any — is actually costing you today.

Read next

Members download the scoped-profiler snippet and a sample capture to practice reading. See membership →

← Back to the notebook