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.
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.
- 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.
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
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íneaperf 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.
- Escribe el recorrido de matriz por filas y por columnas, y cronometra la diferencia con
perf stat. - Mira el
cache-missesde cada versión y correlaciónalo con el tiempo. - Compila un bucle numérico con
-O3y comprueba si el compilador lo vectorizó (mira el ensamblador). - Perfila un programa tuyo con
perf record/reporty encuentra su verdadero cuello de botella.