Optimización de rendimiento en C/C++

By Carlos Montiel | Especialista en IA Empresarial
Publicado: 2026-07-28 | Por: Carlos Montiel | Lectura: ~4 minutos

Un juego que funciona no es lo mismo que un juego que funciona bien. Hoy aprendemos a medir dónde se va el tiempo de CPU, y a recuperarlo con cambios concretos.

La regla de oro: mide antes de optimizar

En la lección anterior nos enfocamos en que el juego multijugador funcionara correctamente. Ahora toca la pregunta de rendimiento — y la regla más importante de toda esta lección es: nunca optimices a ciegas. La intuición sobre "qué es lento" en C++ falla sorprendentemente seguido. Necesitas medir con datos reales antes de cambiar una sola línea.

#include <chrono> #include <iostream> class Cronometro { public: Cronometro(const char* etiqueta) : etiqueta(etiqueta) { inicio = std::chrono::high_resolution_clock::now(); } ~Cronometro() { auto fin = std::chrono::high_resolution_clock::now(); auto duracion = std::chrono::duration_cast<std::chrono::microseconds>(fin - inicio); std::cout << etiqueta << ": " << duracion.count() << " microsegundos\n"; } private: const char* etiqueta; std::chrono::high_resolution_clock::time_point inicio; }; // Uso: al salir de scope, el destructor imprime el tiempo transcurrido. void actualizar_enemigos() { Cronometro medir("actualizar_enemigos"); // ... lógica de actualización ... }

Para mediciones más profundas, herramientas como `perf` en Linux o Valgrind/Callgrind te muestran exactamente qué función consume más tiempo de CPU, sin adivinar.

# Perfilar el binario y ver un resumen de tiempo por función perf record ./juego perf report # Detectar fugas de memoria y accesos inválidos valgrind --leak-check=full ./juego

Banderas del compilador: la optimización más barata

Antes de tocar una sola línea de tu código, asegúrate de estar compilando en modo release con optimizaciones activadas. La diferencia entre compilar sin optimizar y con `-O2` puede ser de varias veces en rendimiento, sin cambiar nada de tu lógica.

# Modo debug (para desarrollar, con símbolos de depuración) g++ -g -O0 main.cpp -o juego_debug # Modo release (para medir rendimiento real y para distribuir) g++ -O2 -DNDEBUG main.cpp -o juego_release

En CMake, esto se controla con `CMAKE_BUILD_TYPE`:

cmake -DCMAKE_BUILD_TYPE=Release ..

Un error común de principiante es medir el rendimiento de su juego compilado en modo debug y sacar conclusiones equivocadas — código sin optimizar puede ser 5 a 10 veces más lento que el mismo código optimizado.

Localidad de caché: por qué el orden de tus datos importa

La CPU no lee memoria de a un byte; lee en bloques (líneas de caché, típicamente 64 bytes). Si tus datos están dispersos en memoria (por ejemplo, un `std::vector<Enemigo*>` con punteros a objetos alocados en cualquier lugar del heap), cada acceso puede ser un "cache miss" costoso. Si en cambio tienes un `std::vector<Enemigo>` con los objetos contiguos en memoria, recorrerlos es mucho más rápido.

// Más lento: cada Enemigo* puede estar en cualquier parte de la memoria std::vector<Enemigo*> enemigos_dispersos; // Más rápido: los Enemigo están contiguos en memoria, la CPU los prefetch-ea bien std::vector<Enemigo> enemigos_contiguos; // Recorrer la versión contigua es más amigable con el caché: for (Enemigo& e : enemigos_contiguos) { e.x += e.velocidad_x * delta; }

Este patrón de organizar datos por cómo se acceden (en vez de por conveniencia orientada a objetos) se conoce como "diseño orientado a datos" (data-oriented design), y es una de las técnicas de mayor impacto en rendimiento para juegos con muchas entidades.

Evitando asignaciones dinámicas dentro del bucle del juego

Llamar a `new`, `malloc`, o incluso hacer `push_back` en un `std::vector` que necesita crecer, son operaciones relativamente costosas si ocurren muchas veces por segundo. La técnica estándar es reservar la memoria una sola vez, por adelantado.

// Problemático: puede reasignar memoria repetidamente si crece sin control std::vector<Proyectil> proyectiles; void disparar() { proyectiles.push_back(Proyectil{}); } // Mejor: reserva la capacidad esperada una sola vez, al iniciar el nivel std::vector<Proyectil> proyectiles; proyectiles.reserve(200); // evita realocaciones mientras el vector no supere 200 // Aún mejor para un "pool" de objetos: array de tamaño fijo con reciclaje struct PoolProyectiles { Proyectil datos[200]; bool activo[200] = {false}; int obtener_libre() { for (int i = 0; i < 200; ++i) { if (!activo[i]) return i; } return -1; // pool lleno } };

Un "object pool" como este evita por completo las asignaciones dinámicas durante el juego: reutilizas espacios ya reservados en vez de crear y destruir objetos constantemente, algo especialmente notorio en juegos con muchos proyectiles o partículas.

Paso de tiempo fijo vs. variable

En la lección 3 usamos `delta` variable para mover objetos de forma proporcional al tiempo real. Esto es correcto para renderizado, pero puede introducir inconsistencias físicas sutiles (colisiones que se comportan distinto según el framerate). La solución estándar en motores serios es un paso de tiempo fijo para la física, desacoplado del framerate de dibujado.

const float PASO_FIJO = 1.0f / 60.0f; // física siempre a 60Hz, sin importar el framerate real float tiempo_acumulado = 0.0f; // dentro del game loop: tiempo_acumulado += delta_real; while (tiempo_acumulado >= PASO_FIJO) { actualizar_fisica(PASO_FIJO); // siempre el mismo delta, resultados reproducibles tiempo_acumulado -= PASO_FIJO; } dibujar(); // el dibujado sí puede variar con el framerate real

Usando tu asistente de IA para optimizar con criterio

La forma más efectiva de usar un asistente de IA en esta lección no es pedirle "optimiza mi juego" en abstracto — sin datos de perfilado, cualquier sugerencia es una adivinanza. En cambio, corre `perf` o Valgrind primero, y pégale a tu asistente el reporte real: qué función consume más tiempo, cuántas veces se llama, y el código fuente de esa función específica. Con esos datos concretos, un asistente de IA puede sugerir cambios específicos y explicarte el porqué — por ejemplo, detectar que estás copiando un `struct` grande por valor en cada llamada en vez de pasarlo por referencia constante.

Con el rendimiento bajo control, en la próxima lección vamos a abordar una pregunta que probablemente te has hecho durante todo el curso: ¿cuándo conviene realmente dar el salto de C puro a C++, y qué se gana (y se pierde) al hacerlo?

Carlos Montiel
Arquitecto de Soluciones IA Empresarial
Especialista en LLMs, Agentes y Orquestación
guatemalia.com/#contacto · info@guatemalia.com

¿Necesitas implementar IA en tu empresa?

Carlos Montiel es arquitecto de soluciones IA empresarial. Implementa LLMs, Agentes, RAG y orquestadores en empresas de Guatemala y Latinoamérica. Contáctalo para una consultoría.

Contactar a Carlos Montiel

info@guatemalia.com