Zurück zur Startseite

Code-Benchmarking: Präzise CPU- und Cache-Messungen

Kapitel zum Erstellen eines Benchmarking-Frameworks mit hochpräzisen CPU-Zyklusmessungen und perf-Zählern. Beispiele für x86, RISC-V, ARM. Statistische Analyse und typische Fehler.

Präzise Benchmarks: von rdtsc bis zur perf-Analyse
Advertisement 728x90

Benchmarking und Profiling: Genaue Messungen der Code-Performance

Code-Optimierung ohne objektive Daten führt zu Fehlern. Eine umgeschriebene Hash-Funktion, die angeblich die Cache-Lokalität verbessern sollte, verlangsamte die Ausführung um 15 %. Die Intuition ist trügerisch – präzise Messungen von Zeit und Prozessorereignissen sind erforderlich.

Ein Benchmarking-Framework löst das Problem: Mehrfache Durchläufe, statistische Analyse, Integration mit perf. Damit lassen sich echte Engpässe identifizieren.

Hochpräzise Timing-Methoden

Die Standardfunktion time() bietet eine Auflösung von 1 Sekunde – unakzeptabel für Mikro-Optimierungen.

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

Vorteile: Nanosekunden-Auflösung, resistent gegen Systemzeit-Anpassungen, POSIX-Portabilität.

CPU Cycle Counters (empfohlen)

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

Auflösung – 1 Zyklus, kein Overhead durch Systemaufrufe. Nachteile: architekturspezifisch, beeinflusst durch Frequenzänderungen.

Statistische Auswertung der Ergebnisse

Ein einzelner Durchlauf ist nutzlos wegen Cache-Variationen, OS-Unterbrechungen, Branching.

Google AdInline article slot

Basisstatistiken

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

Erweiterte Statistiken

Der Median ist robust gegenüber Ausreißern, die Standardabweichung zeigt die Stabilität:

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

Wichtige Metriken:

  • Minimum: Bestfall (warmer Cache)
  • Median: typische Leistung
  • Stddev: Variabilität
  • Maximum: schlechtester Fall

Benchmarking-Framework

Universelle Schnittstelle für die Wiederverwendung:

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

void benchmark_run(benchmark_t *bench, int iterations);

Implementierung umfasst:

  • Aufwärmdurchlauf
  • Mehrfache Messungen
  • Statistische Auswertung

Beispiel:

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

Cache-Analyse mit perf

perf stat erfasst Hardware-Zähler:

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

Nützliche Events:

  • cache-references/misses: alle Ebenen
  • L1-dcache-loads/misses: L1-Daten
  • LLC-loads/misses: letzte Ebene

Strukturvergleich: Array – 1,2K Misses, verkettete Liste – 45K Misses (37x schlechter).

Integration von perf ins 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;

Automatische Erfassung und Aggregation von perf_event_open-Zählern.

Häufige Probleme und Lösungen

Compiler-Optimierungen

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

// Good
volatile int sum = 0;

Kalter vs. warmer Cache

Erster Durchlauf ist langsamer. Lösung: Aufwärmen + separate Metrik.

Messoverhead

rdcycle() benötigt ~10 Zyklen. Abziehen oder lange Tests verwenden.

Systemrauschen

  • Viele Iterationen
  • Median statt Mittelwert
  • cpupower frequency-set -g performance
  • taskset -c 0
  • nice -n -20

Was zählt

  • CPU-Zähler: rdtsc/rdcycle – Goldstandard für Genauigkeit
  • Statistiken: Median + Stddev zuverlässiger als Mittelwert
  • perf-Events: Cache-Misses enthüllen Lokalitätsprobleme
  • Cache-Aufwärmen: essenziell für realistische Messungen
  • Volatile: verhindert Dead-Code-Optimierung

— Editorial Team

Advertisement 728x90

Weiterlesen