Volver al inicio

Benchmarking de Código: Mediciones Precisas de CPU y Caché

Capítulo sobre la creación de un framework de benchmarking con mediciones de ciclos de CPU de alta precisión y contadores perf. Ejemplos para x86, RISC-V, ARM. Análisis estadístico y errores típicos.

Benchmarks Precisos: desde rdtsc hasta análisis perf
Advertisement 728x90

# # Benchmarking y Perfilado: Mediciones Precisas del Rendimiento del Código

Optimizar código sin datos objetivos lleva a errores. Una función hash reescrita, supuestamente para mejorar la localidad de caché, ralentizó la ejecución en un 15 %. La intuición es engañosa: se necesitan mediciones precisas de tiempo y eventos del procesador.

Un marco de benchmarking resuelve el problema: múltiples ejecuciones, análisis estadístico, integración con perf. Esto permite identificar los verdaderos cuellos de botella.

Métodos de Temporización de Alta Precisión

La función estándar time() proporciona una resolución de 1 segundo, inaceptable para microoptimizaciones.

Google AdInline article slot

clock_gettime()

struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
run_test();
clock_gettime(CLOCK_MONOTONIC, &end);

long ns = (end.tv_sec - start.tv_sec) * 1000000000L +
          (end.tv_nsec - start.tv_nsec);

Ventajas: resolución de nanosegundos, resistente a ajustes de hora del sistema, portabilidad POSIX.

Contadores de Ciclos de CPU (recomendado)

RISC-V:

static inline uint64_t rdcycle(void) {
    uint64_t cycles;
    asm volatile ("rdcycle %0" : "=r" (cycles));
    return cycles;
}

x86_64:

Google AdInline article slot
static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    asm volatile ("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}

ARM64:

static inline uint64_t rdcycle(void) {
    uint64_t val;
    asm volatile("mrs %0, pmccntr_el0" : "=r"(val));
    return val;
}

Resolución de 1 ciclo, sin sobrecarga de llamada al sistema. Desventajas: específico de arquitectura, afectado por cambios de frecuencia.

Análisis Estadístico de Resultados

Una sola ejecución es inútil debido a variaciones de caché, interrupciones del SO, ramificaciones.

Google AdInline article slot

Estadísticas Básicas

#define ITERATIONS 1000
uint64_t times[ITERATIONS];
// ... fill times ...

uint64_t min = times[0], max = times[0], sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
    if (times[i] < min) min = times[i];
    if (times[i] > max) max = times[i];
    sum += times[i];
}
uint64_t mean = sum / ITERATIONS;

Estadísticas Avanzadas

La mediana es robusta ante valores atípicos, la desviación estándar indica estabilidad:

qsort(times, ITERATIONS, sizeof(uint64_t), compare_uint64);
uint64_t median = times[ITERATIONS / 2];

double variance = 0;
for (int i = 0; i < ITERATIONS; i++) {
    double diff = (double)times[i] - (double)mean;
    variance += diff * diff;
}
double stddev = sqrt(variance / ITERATIONS);

Métricas Clave:

  • Mínimo: mejor caso (caché caliente)
  • Mediana: rendimiento típico
  • Desviación estándar: variabilidad
  • Máximo: peor caso

Marco de Benchmarking

Interfaz universal para reutilización:

typedef struct {
    const char *name;
    void (*setup)(void);
    void (*run)(void);
    void (*teardown)(void);
} benchmark_t;

void benchmark_run(benchmark_t *bench, int iterations);

La implementación incluye:

  • Ejecución de calentamiento
  • Múltiples mediciones
  • Informe estadístico

Ejemplo:

int array[1000];

void setup_array(void) {
    for (int i = 0; i < 1000; i++) {
        array[i] = i;
    }
}

void test_sequential_access(void) {
    volatile int sum = 0;
    for (int i = 0; i < 1000; i++) {
        sum += array[i];
    }
}

benchmark_t bench = {
    .name = "Sequential Array Access",
    .setup = setup_array,
    .run = test_sequential_access,
    .teardown = NULL
};

benchmark_run(&bench, 1000);

Análisis de Caché con perf

perf stat rastrea contadores de hardware:

$ perf stat -e cache-references,cache-misses ./program
  1,234,567 cache-references
     12,345 cache-misses # 1.00% miss rate

Eventos Útiles:

  • cache-references/misses: todos los niveles
  • L1-dcache-loads/misses: datos L1
  • LLC-loads/misses: último nivel

Comparación de Estructuras: array: 1,2K fallos de caché, lista enlazada: 45K fallos de caché (37 veces peor).

Integrando perf en el Marco

typedef struct {
    uint64_t cycles;
    uint64_t cache_references;
    uint64_t cache_misses;
    uint64_t l1_loads;
    uint64_t l1_misses;
} perf_counters_t;

Recopilación y agregación automática de contadores de perf_event_open.

Problemas Comunes y Soluciones

Optimizaciones del Compilador

// Malo: bucle eliminado
int sum = 0;
for (...) sum += array[i];

// Bueno
volatile int sum = 0;

Caché Fría vs Caché Caliente

La primera ejecución es más lenta. Solución: calentamiento + métrica separada.

Sobrecarga de Medición

rdcycle() toma ~10 ciclos. Restar o usar pruebas largas.

Ruido del Sistema

  • Muchas iteraciones
  • Mediana en lugar de media
  • cpupower frequency-set -g performance
  • taskset -c 0
  • nice -n -20

Lo que Importa

  • Contadores de CPU: rdtsc/rdcycle: estándar de oro para precisión
  • Estadísticas: mediana + desviación estándar más fiable que la media
  • Eventos de perf: fallos de caché revelan problemas de localidad
  • Calentamiento de Caché: esencial para mediciones realistas
  • Volatile: evita optimización de código muerto

— Editorial Team

Advertisement 728x90

Leer después