Retour à l'accueil

Benchmarking de code : Mesures précises du CPU et du cache

Chapitre sur la création d'un framework de benchmarking avec mesures de cycles CPU haute précision et compteurs perf. Exemples pour x86, RISC-V, ARM. Analyse statistique et erreurs typiques.

Benchmarks précis : de rdtsc à l'analyse perf
Advertisement 728x90

Benchmarking et profilage : Mesures précises des performances du code

Optimiser du code sans données objectives conduit à des erreurs. Une fonction de hachage réécrite, censée améliorer la localité de cache, a ralenti l'exécution de 15 %. L'intuition est trompeuse — des mesures précises du temps et des événements du processeur sont nécessaires.

Un framework de benchmarking résout le problème : plusieurs exécutions, analyse statistique, intégration avec perf. Cela permet d'identifier les vrais goulots d'étranglement.

Méthodes de chronométrage haute précision

La fonction standard time() offre une résolution de 1 seconde — inacceptable pour les micro-optimisations.

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);

Avantages : résolution à la nanoseconde, résistante aux ajustements de l'heure système, portabilité POSIX.

Compteurs de cycles CPU (recommandé)

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;
}

Résolution — 1 cycle, pas de surcharge d'appel système. Inconvénients : spécifique à l'architecture, affecté par les changements de fréquence.

Analyse statistique des résultats

Une seule exécution est inutile en raison des variations de cache, des interruptions du système d'exploitation, des branchements.

Google AdInline article slot

Statistiques de base

#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;

Statistiques avancées

La médiane est robuste face aux valeurs aberrantes, l'écart-type indique la stabilité :

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étriques clés :

  • Minimum : meilleur cas (cache chaud)
  • Médiane : performance typique
  • Écart-type : variabilité
  • Maximum : pire cas

Framework de benchmarking

Interface universelle pour réutilisation :

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

void benchmark_run(benchmark_t *bench, int iterations);

L'implémentation inclut :

  • Exécution d'échauffement
  • Mesures multiples
  • Rapport statistique

Exemple :

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);

Analyse de cache avec perf

perf stat suit les compteurs matériels :

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

Événements utiles :

  • cache-references/misses : tous les niveaux
  • L1-dcache-loads/misses : données L1
  • LLC-loads/misses : dernier niveau

Comparaison de structures : tableau—1,2 K misses, liste chaînée—45 K misses (37x pire).

Intégration de perf dans le framework

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

Collecte et agrégation automatique des compteurs perf_event_open.

Problèmes courants et solutions

Optimisations du compilateur

// Bad: loop eliminated
int sum = 0;
for (...) sum += array[i];

// Good
volatile int sum = 0;

Cache froid vs cache chaud

La première exécution est plus lente. Solution : échauffement + métrique séparée.

Surcharge de mesure

rdcycle() prend ~10 cycles. Soustraire ou utiliser des tests longs.

Bruit système

  • De nombreuses itérations
  • Médiane au lieu de moyenne
  • cpupower frequency-set -g performance
  • taskset -c 0
  • nice -n -20

Ce qui compte

  • Compteurs CPU : rdtsc/rdcycle — étalon-or pour la précision
  • Statistiques : médiane + écart-type plus fiable que la moyenne
  • Événements perf : cache-misses révèle les problèmes de localité
  • Échauffement du cache : essentiel pour des mesures réalistes
  • Volatile : empêche l'optimisation de code mort

— Editorial Team

Advertisement 728x90

Lire ensuite