Performance Optimization in C/C++

By Carlos Montiel | Enterprise AI Specialist
Leer en español →
Published: 2026-07-28 | By: Carlos Montiel | Reading time: ~4 minutes

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.

The Golden Rule: Measure Before You Optimize

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.

#include <chrono> #include <iostream> class Timer { public: Timer(const char* label) : label(label) { start = std::chrono::high_resolution_clock::now(); } ~Timer() { auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start); std::cout << label << ": " << duration.count() << " microseconds\n"; } private: const char* label; std::chrono::high_resolution_clock::time_point start; }; // Usage: when it goes out of scope, the destructor prints the elapsed time. void update_enemies() { Timer measure("update_enemies"); // ... update logic ... }

For deeper measurements, tools like `perf` on Linux or Valgrind/Callgrind show you exactly which function consumes the most CPU time, no guessing involved.

# Profile the binary and see a time-per-function summary perf record ./game perf report # Detect memory leaks and invalid accesses valgrind --leak-check=full ./game

Compiler Flags: the Cheapest Optimization

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.

# Debug mode (for development, with debug symbols) g++ -g -O0 main.cpp -o game_debug # Release mode (for measuring real performance and for distribution) g++ -O2 -DNDEBUG main.cpp -o game_release

In CMake, this is controlled with `CMAKE_BUILD_TYPE`:

cmake -DCMAKE_BUILD_TYPE=Release ..

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.

Cache Locality: Why the Order of Your Data Matters

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.

// Slower: each Enemy* can be anywhere in memory std::vector<Enemy*> scattered_enemies; // Faster: Enemy objects are contiguous in memory, the CPU prefetches them well std::vector<Enemy> contiguous_enemies; // Iterating the contiguous version is much more cache-friendly: for (Enemy& e : contiguous_enemies) { e.x += e.velocity_x * delta; }

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.

Avoiding Dynamic Allocations Inside the Game Loop

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.

// Problematic: can reallocate memory repeatedly if it grows unchecked std::vector<Projectile> projectiles; void fire() { projectiles.push_back(Projectile{}); } // Better: reserve the expected capacity once, when the level starts std::vector<Projectile> projectiles; projectiles.reserve(200); // avoids reallocations as long as the vector stays under 200 // Even better for an object "pool": fixed-size array with recycling struct ProjectilePool { Projectile data[200]; bool active[200] = {false}; int get_free_slot() { for (int i = 0; i < 200; ++i) { if (!active[i]) return i; } return -1; // pool full } };

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.

Fixed vs. Variable Time Step

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.

const float FIXED_STEP = 1.0f / 60.0f; // physics always at 60Hz, regardless of real framerate float accumulated_time = 0.0f; // inside the game loop: accumulated_time += real_delta; while (accumulated_time >= FIXED_STEP) { update_physics(FIXED_STEP); // always the same delta, reproducible results accumulated_time -= FIXED_STEP; } draw(); // drawing can still vary with the real framerate

Using Your AI Assistant to Optimize with Judgment

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
Enterprise AI Solutions Architect
Specialist in LLMs, Agents, and Orchestration
guatemalia.com/en/#contact · info@guatemalia.com

Need to implement AI at your company?

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

info@guatemalia.com