¿Cómo se mide la optimización de rendimiento x86-64?
Validar uops, dependencias, cachés y predicción de saltos con perfiles y benchmarks
¿Cómo se mide la optimización de rendimiento x86-64?
Este artículo se ha creado con fines didácticos. El rendimiento cambia con microarquitectura, energía y temperatura, jerarquía de memoria, SO y entrada. No convierta un ciclo fijo en regla universal.
Menos instrucciones no garantizan más velocidad. Una instrucción puede generar varios uops, las independientes pueden solaparse y un cache miss o una dependencia larga puede dominar el bucle. Optimizar es medir el cuello de botella, formular una hipótesis causal e intentar refutarla con un experimento controlado.
Latencia y throughput
Latencia es el tiempo hasta que una operación dependiente usa el resultado; throughput es la frecuencia de inicio de operaciones independientes.
; dependency chain
add rax, rbx
add rax, rcx
add rax, rdx
; independent
add r8, rbx
add r9, rcx
add r10, rdx
Los mismos tres add tienen caminos críticos distintos. El benchmark debe decir qué mide.
Cuellos de botella por niveles
| Nivel | Ejemplos |
|---|---|
| Front-end | instruction cache, decode, uop cache |
| Execution | ports, latencia, dependencias, divider |
| Memory | cache/DRAM/TLB, store forwarding |
| Control | branch misprediction, objetivos indirectos |
| System | page fault, scheduler, locks, I/O |
Líneas de fuente, número de instrucciones, bytes y saltos son indicios, no mediciones de tiempo. Confirme primero el hot path con wall time y profiler.
Localidad y distribución de saltos
Arrays contiguos aprovechan cache lines y prefetch. Leer un campo de estructuras grandes o hacer pointer chasing desperdicia ancho de banda y limita paralelismo. AoS→SoA o reducir working set puede superar a ASM.
Un salto casi siempre igual es predecible. Datos irregulares pueden favorecer cmov o máscaras, pero añaden trabajo y dependencias. Un benchmark aleatorio distinto del producto puede engañar.
Contrato del benchmark
Documente hipótesis, tamaños y distribución, prevención de dead-code elimination, setup incluido, cache frío/caliente e independencia. Registre CPU, microcode, compiler/options, ISA, LTO/PGO, seed, affinity, energía, temperatura, repeticiones, distribución y carga del SO. Use varios runs, no solo el mejor.
rdtsc no es un temporizador automático: importan out-of-order, serialización, migración e invariant TSC. Empiece con framework fiable y reloj monotónico.
Profiler, counters y compilador
El profiler localiza tiempo; los counters prueban hipótesis de cycles, branches, cache y TLB. Los eventos dependen de CPU y sufren multiplexing, skid y especulación. IPC solo no decide calidad.
Orden: referencia C/C++ correcta, salida Release, algoritmo/data layout, PGO/target/intrinsics y solo después ASM en un hot loop estrecho. Una función ASM separada puede perder inlining y optimización interprocedural.
Antes de medir, pruebe 0, 1, límites vectoriales, tamaños grandes, overflow, alignment, alias/overlap, especiales FP, fallback de funciones, sanitizer y unwind.
Resumen
El rendimiento x86-64 combina latencia, throughput, uops, recursos, dependencias, cache/TLB, predicción y ruido del sistema. Localice con perfil, valide una hipótesis precisa y conserve ASM solo cuando beneficio y mantenimiento sean reproducibles.