Stosy i kolejki: optymalizacja pod kątem pamięci podręcznej i wydajności w C
Stos wywołań jest obecny w każdym programie, ale przy implementacji własnych stosów i kolejek wybór struktury danych krytycznie wpływa na wydajność. Listy powiązane powodują nawet do 37 razy więcej cykli z powodu chybień pamięci podręcznej i narzutu malloc/free. Bufor kołowy na tablicy zapewnia przewidywalne zużycie pamięci i przyspieszenie do 35 razy.
Implementacja stosu: tablica vs lista powiązana
Klasyczne akademickie implementacje stosów różnią się pod względem zachowania pamięci podręcznej.
Stos na tablicy (stały rozmiar, 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;
}
Stos na liście powiązanej (dynamiczny, 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;
}
Benchmarki (1000 operacji):
| Implementacja | Cykle | Chybienia pamięci podręcznej |
|---------------|---------|------------------------------|
| Tablica | 12 tys. | 45 |
| Lista | 450 tys.| 2100 |
Przyczyny spowolnienia listy powiązanej:
- Narzut
malloc/free(~100 taktów na operację) - Rozproszenie węzłów po stercie (chybienia L1/L2)
- Podążanie za wskaźnikami (zależność danych)
Rekomendacje wyboru:
- Tablica: systemy wbudowane, czas rzeczywisty
- Lista: nieprzewidywalny rozmiar + nadmiar pamięci
Bufor kołowy: podstawa wydajnych kolejek
Naiwna kolejka na tablicy psuje się po osiągnięciu końca bufora:
void enqueue(queue_t *q, int value) {
if (q->rear < MAX_SIZE) {
q->data[q->rear++] = value;
}
}
Problem: front==rear nie rozróżnia kolejki pustej i pełnej.
Bufor kołowy rozwiązuje problem za pomocą arytmetyki modularnej:
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;
}
Benchmarki (1M operacji):
| Implementacja | Cykle | Chybienia pamięci podręcznej |
|---------------|------------|------------------------------|
| Bufor kołowy | 15 mln | 1234 |
| Lista | 520 mln | 980 tys. |
Przyspieszenie 35 razy dzięki lokalności pamięci podręcznej.
Optymalizacja bufora kołowego
Operacja % (10-40 taktów) — wąskie gardło.
Optymalizacja 1: rozmiar będący potęgą dwójki
#define MAX_SIZE 1024
#define MASK (MAX_SIZE - 1)
q->tail = (q->tail + 1) & MASK; // 1 takt zamiast 30
Rezultat: przyspieszenie 1.76 razy (15M → 8.5M taktów).
Optymalizacja 2: bez licznika count
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;
}
Kompromis: maksymalnie MAX_SIZE-1 elementów.
Bufor kołowy bez blokad (SPSC)
Dla pojedynczego producenta/konsumenta (przerwania, rdzenie):
typedef struct {
volatile int data[MAX_SIZE];
volatile int head; // Tylko konsument
volatile int tail; // Tylko producent
} 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;
}
}
Kluczowe elementy:
volatileprzeciwko optymalizacjom kompilatora- Bariery pamięci dla słabych modeli (ARM, RISC-V)
- Brak operacji atomowych
Wersja dla RISC-V:
asm volatile("fence w, w" ::: "memory");
Kolejka priorytetowa: kopiec binarny
Min-kopiec (element minimalny w korzeniu):
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;
// zamiana
int temp = h->data[i];
h->data[i] = h->data[parent];
h->data[parent] = temp;
i = parent;
}
}
Złożoność: O(log n), dobra lokalność pamięci podręcznej przy małych n.
Podsumowanie
- Listy powiązane dla stosów/kolejek dają 30-40x spowolnienie z powodu chybień pamięci podręcznej
- Bufor kołowy z rozmiarem 2^n przyspiesza 1.76x dzięki operacji bitowego AND
- Bufor bez blokad SPSC idealny dla przerwań i RTOS
- Kopce binarne zachowują zalety tablic przy O(log n)
- W systemach wbudowanych stały rozmiar = determinizm
— Editorial Team
Brak komentarzy.