A game that works isn't the same as a game that works well. Today we learn to measure where CPU time actually goes, and to reclaim it with concrete changes.
In the previous lesson we focused on getting the multiplayer game working correctly. Now comes the performance question — and the single most important rule in this whole lesson is: never optimize blindly. Intuition about "what's slow" in C++ fails surprisingly often. You need to measure with real data before changing a single line.
For deeper measurements, tools like `perf` on Linux or Valgrind/Callgrind show you exactly which function consumes the most CPU time, no guessing involved.
Before touching a single line of your code, make sure you're compiling in release mode with optimizations enabled. The difference between compiling unoptimized and with `-O2` can be several times over in performance, without changing any of your logic.
In CMake, this is controlled with `CMAKE_BUILD_TYPE`:
A common beginner mistake is measuring your game's performance in debug mode and drawing the wrong conclusions — unoptimized code can be 5 to 10 times slower than the same optimized code.
The CPU doesn't read memory one byte at a time; it reads in blocks (cache lines, typically 64 bytes). If your data is scattered across memory (for example, a `std::vector<Enemy*>` with pointers to objects allocated anywhere on the heap), every access can be a costly "cache miss." If instead you have a `std::vector<Enemy>` with the objects contiguous in memory, iterating over them is much faster.
This pattern of organizing data by how it's accessed (instead of by object-oriented convenience) is known as "data-oriented design," and it's one of the highest-impact performance techniques for games with many entities.
Calling `new`, `malloc`, or even doing `push_back` on a `std::vector` that needs to grow, are relatively expensive operations if they happen many times per second. The standard technique is to reserve the memory once, up front.
An "object pool" like this completely avoids dynamic allocations during gameplay: you reuse already-reserved slots instead of constantly creating and destroying objects, something especially noticeable in games with lots of projectiles or particles.
In lesson 3 we used a variable `delta` to move objects proportionally to real time. This is correct for rendering, but can introduce subtle physics inconsistencies (collisions that behave differently depending on framerate). The standard solution in serious engines is a fixed time step for physics, decoupled from the rendering framerate.
The most effective way to use an AI assistant in this lesson isn't asking it to "optimize my game" in the abstract — without profiling data, any suggestion is a guess. Instead, run `perf` or Valgrind first, and paste your assistant the real report: which function consumes the most time, how many times it's called, and the source code of that specific function. With that concrete data, an AI assistant can suggest specific changes and explain why — for example, spotting that you're copying a large `struct` by value on every call instead of passing it by const reference.
With performance under control, in the next lesson we'll tackle a question you've probably been asking yourself throughout the course: when does it actually make sense to move from plain C to C++, and what do you gain (and lose) by doing it?
Carlos Montiel is an enterprise AI solutions architect. He implements LLMs, Agents, RAG, and orchestrators for companies across Guatemala and Latin America. Reach out for a consultation.
Contact Carlos Montiel