wandres.dev
OPTIMIZADOR · Bajo nivel y rendimiento

Bajo nivel y rendimiento

Inline assembly, SIMD, y lo que de verdad determina la velocidad: la jerarquía de caché. Optimizar guiado por datos con perf, no por corazonadas.

⏱ 15 min

Llega el momento de exprimir la máquina. Pero optimizar bien no es meter trucos: es entender qué frena de verdad al hardware —casi siempre, la memoria— y medir en vez de adivinar. Aquí está la diferencia entre creer que optimizas y hacerlo de verdad.

🎯 Al terminar esta lección sabrás
  • La jerarquía de caché: el factor que domina el rendimiento.
  • Diseño orientado a datos.
  • SIMD e intrinsics.
  • Medir con perf en vez de adivinar.

La caché lo es casi todo

El mito del rendimiento es que importan las instrucciones. La realidad: importa la memoria. Un acceso a la caché L1 tarda ~1 ns; uno a la RAM principal, ~100 ns. Cien veces más. Tu CPU pasa la mayor parte del tiempo esperando datos, no calculando.

// recorrer una matriz por filas vs por columnas: MISMO trabajo, ORDEN distinto
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        suma += m[i][j];        // por filas: memoria contigua → caché feliz

for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        suma += m[i][j];        // por columnas: saltos → fallo de caché por acceso

El segundo bucle puede ser varias veces más lento que el primero, haciendo exactamente la misma suma. La única diferencia es el patrón de acceso a memoria.

Diseño orientado a datos: organiza para la caché

Esta es la idea que transforma tu forma de optimizar. La CPU no trae bytes sueltos: trae líneas de caché (64 bytes) enteras. Si tus datos relacionados están contiguos, un solo acceso los trae todos; si están dispersos, cada uno cuesta un viaje a la RAM. De ahí nace el diseño orientado a datos: organiza tus estructuras según cómo las recorres, no según una jerarquía conceptual bonita. Un array de structs (AoS) vs un struct de arrays (SoA) puede significar 10× de diferencia si solo tocas un campo al iterar. Los motores de juego, las bases de datos y el HPC se diseñan alrededor de la caché. Recuerda el nivel 11 (ordenar campos, reducir padding): era esto. Optimizar de verdad es, sobre todo, ser amable con la caché.

SIMD e intrinsics

Las CPUs pueden operar sobre varios datos con una instrucción (Single Instruction, Multiple Data): sumar 8 floats de golpe. El compilador vectoriza automáticamente con -O3, pero puedes hacerlo explícito con intrinsics:

#include <immintrin.h>
__m256 a = _mm256_loadu_ps(x);      // carga 8 floats
__m256 b = _mm256_loadu_ps(y);
__m256 c = _mm256_add_ps(a, b);     // los suma los 8 a la vez
_mm256_storeu_ps(z, c);

Y para lo verdaderamente específico, inline assembly te deja escribir instalaciones concretas (raro, pero posible):

int r;
__asm__ ("rdtsc" : "=a"(r));   // leer el contador de ciclos, por ejemplo

Mide, no adivines

💡
La regla de oro: perfila antes de optimizar

El error más caro en optimización es optimizar por intuición. Los cuellos de botella casi nunca están donde crees. La disciplina profesional: mide primero con un profiler, encuentra el 3% del código donde se va el 97% del tiempo, optimiza SOLO eso, y vuelve a medir para confirmar que ganaste. perf es la herramienta en Linux:

perf stat ./prog                  # ciclos, fallos de caché, instrucciones
perf record ./prog && perf report # dónde se va el tiempo, línea a línea

perf stat te muestra los fallos de caché (cache-misses) — el número que más correlaciona con la lentitud real. Optimizar sin medir es reorganizar código al azar; con perf, es ingeniería.

⚔️ Optimiza con datos
  1. Escribe el recorrido de matriz por filas y por columnas, y cronometra la diferencia con perf stat.
  2. Mira el cache-misses de cada versión y correlaciónalo con el tiempo.
  3. Compila un bucle numérico con -O3 y comprueba si el compilador lo vectorizó (mira el ensamblador).
  4. Perfila un programa tuyo con perf record/report y encuentra su verdadero cuello de botella.