Zurück zur Startseite

Stapel & Warteschlangen: Cache-Optimierung für Leistung in C

Dieser Artikel vergleicht die Leistung von Array- und verketteten Listen-basierten Implementierungen für Stapel und Warteschlangen in C. Er zeigt, wie Cache-Misses und Speicher-Overhead die Performance um das 30-40-fache beeinträchtigen können und stellt optimierte Ansätze wie Ringpuffer mit Zweierpotenz-Größe und lock-free SPSC-Puffer für Echtzeitanwendungen vor.

Cache-Optimierung: Stapel & Warteschlangen in C bis zu 35x schneller
Advertisement 728x90

Stapel und Warteschlangen: Cache-Optimierung und Leistung in C

Der Aufrufstapel ist in jedem Programm vorhanden, aber bei der Implementierung eigener Stapel und Warteschlangen beeinflusst die Wahl der Datenstruktur die Leistung entscheidend. Verkettete Listen können aufgrund von Cache-Misses und malloc/free-Overhead bis zu 37-mal mehr Zyklen verursachen. Ringpuffer auf Basis von Arrays bieten vorhersehbaren Speicherverbrauch und eine bis zu 35-fache Beschleunigung.

Stapel-Implementierung: Array vs. Verkettete Liste

Klassische Lehrbuch-Implementierungen für Stapel unterscheiden sich im Cache-Verhalten.

Array-basierter Stapel (feste Größe, O(1)):

Google AdInline article slot
#define MAX_SIZE 1000

typedef struct {
    int data[MAX_SIZE];
    int top;
} stack_t;

void push(stack_t *s, int value) {
    if (s->top < MAX_SIZE) {
        s->data[s->top++] = value;
    }
}

int pop(stack_t *s) {
    if (s->top > 0) {
        return s->data[--s->top];
    }
    return -1;
}

Verkettete-Liste-basierter Stapel (dynamisch, O(1)):

typedef struct node {
    int value;
    struct node *next;
} node_t;

typedef struct {
    node_t *top;
} stack_t;

void push(stack_t *s, int value) {
    node_t *node = malloc(sizeof(node_t));
    node->value = value;
    node->next = s->top;
    s->top = node;
}

int pop(stack_t *s) {
    if (s->top) {
        node_t *node = s->top;
        int value = node->value;
        s->top = node->next;
        free(node);
        return value;
    }
    return -1;
}

Benchmarks (1000 Operationen):

| Implementierung | Zyklen | Cache Misses |

Google AdInline article slot

|----------------|--------|--------------|

| Array | 12K | 45 |

| Verkettete Liste | 450K | 2100 |

Google AdInline article slot

Gründe für die Verlangsamung bei Verketteten Listen:

  • Overhead von malloc/free (~100 Zyklen pro Operation)
  • Knoten sind über den Heap verstreut (L1/L2 Cache Misses)
  • Pointer-Chasing (Datendependenz)

Auswahl-Empfehlungen:

  • Array: Eingebettete Systeme, Echtzeitanwendungen
  • Verkettete Liste: Unvorhersehbare Größe + ausreichend Speicher

Ringpuffer: Die Grundlage effizienter Warteschlangen

Eine naive Array-basierte Warteschlange funktioniert nicht mehr, wenn das Ende des Puffers erreicht ist:

void enqueue(queue_t *q, int value) {
    if (q->rear < MAX_SIZE) {
        q->data[q->rear++] = value;
    }
}

Problem: front==rear kann nicht zwischen einer leeren und einer vollen Warteschlange unterscheiden.

Ringpuffer löst das Problem mithilfe von Modulo-Arithmetik:

typedef struct {
    int data[MAX_SIZE];
    int head;
    int tail;
    int count;
} ring_buffer_t;

void enqueue(ring_buffer_t *q, int value) {
    if (q->count < MAX_SIZE) {
        q->data[q->tail] = value;
        q->tail = (q->tail + 1) % MAX_SIZE;
        q->count++;
    }
}

int dequeue(ring_buffer_t *q) {
    if (q->count > 0) {
        int value = q->data[q->head];
        q->head = (q->head + 1) % MAX_SIZE;
        q->count--;
        return value;
    }
    return -1;
}

Benchmarks (1M Operationen):

| Implementierung | Zyklen | Cache Misses |

|----------------|----------|--------------|

| Ringpuffer | 15M | 1234 |

| Verkettete Liste | 520M | 980K |

Eine 35-fache Beschleunigung dank Cache-Lokalität.

Optimierung des Ringpuffers

Die %-Operation (10-40 Zyklen) ist ein Engpass.

Optimierung 1: Größe als Zweierpotenz

#define MAX_SIZE 1024
#define MASK (MAX_SIZE - 1)

q->tail = (q->tail + 1) & MASK;  // 1 Zyklus statt 30

Ergebnis: 1,76-fache Beschleunigung (15M → 8,5M Zyklen).

Optimierung 2: Eliminierung der Count-Variable

int is_empty(ring_buffer_t *q) {
    return q->head == q->tail;
}

int is_full(ring_buffer_t *q) {
    return ((q->tail + 1) & MASK) == q->head;
}

Kompromiss: maximal MAX_SIZE-1 Elemente.

Lock-Free Ringpuffer (SPSC)

Für Szenarien mit einem einzelnen Produzenten und einem einzelnen Konsumenten (Interrupts, Kerne):

typedef struct {
    volatile int data[MAX_SIZE];
    volatile int head;  // Nur Konsument
    volatile int tail;  // Nur Produzent
} spsc_ring_buffer_t;

void enqueue(spsc_ring_buffer_t *q, int value) {
    int next_tail = (q->tail + 1) & MASK;
    if (next_tail != q->head) {
        q->data[q->tail] = value;
        __sync_synchronize();
        q->tail = next_tail;
    }
}

Schlüsselelemente:

  • volatile zur Verhinderung von Compiler-Optimierungen
  • Speicherbarrieren für schwache Speichermodelle (ARM, RISC-V)
  • Keine atomaren Operationen erforderlich

RISC-V Version:

asm volatile("fence w, w" ::: "memory");

Prioritätswarteschlange: Binärer Heap

Min-Heap (kleinstes Element an der Wurzel):

typedef struct {
    int data[MAX_SIZE];
    int size;
} heap_t;

void heap_push(heap_t *h, int value) {
    int i = h->size++;
    h->data[i] = value;
    while (i > 0) {
        int parent = (i - 1) / 2;
        if (h->data[i] <= h->data[parent]) break;
        // Tausch
        int temp = h->data[i];
        h->data[i] = h->data[parent];
        h->data[parent] = temp;
        i = parent;
    }
}

Komplexität: O(log n), gute Cache-Lokalität für kleine n.

Wichtige Erkenntnisse

  • Verkettete Listen für Stapel/Warteschlangen verursachen 30-40-fache Verlangsamung durch Cache Misses
  • Ringpuffer mit Größe 2^n erreichen 1,76-fache Beschleunigung durch bitweises UND
  • Lock-Free SPSC-Puffer sind ideal für Interrupts und RTOS
  • Binäre Heaps behalten die Vorteile von Arrays bei O(log n)-Komplexität
  • In eingebetteten Systemen bedeutet feste Größe = Determinismus

— Editorial Team

Advertisement 728x90

Weiterlesen