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)):
#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 |
|----------------|--------|--------------|
| Array | 12K | 45 |
| Verkettete Liste | 450K | 2100 |
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:
volatilezur 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
Noch keine Kommentare.