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.
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 :
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.
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 niveauxL1-dcache-loads/misses: données L1LLC-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 performancetaskset -c 0nice -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
Aucun commentaire pour le moment.