Table of Contents
Perché l'allocazione standard della memoria si riduce a breve in codice di alta conformità
Ogni programmatore C si affida a e per la gestione dinamica della memoria. Queste funzioni sono generali-purpose, progettate per lavorare su una vasta gamma di schemi di allocazione, dimensioni degli oggetti e vite. Sotto il cappuccio, gestiscono un heap, mantengono liste libere, blocchi liberi di lavoro adiacenti e allineamento delle maniglie.
Oltre la velocità, la frammentazione è un killer delle prestazioni silenziose. Nel tempo, può spargere piccole allocazioni attraverso il mucchio, lasciando vuoti che non possono essere riutilizzati in modo efficiente. Questo porta ad un maggiore utilizzo della memoria, alle allocazioni future più lente e ai cicli di CPU sprecati.
Questa guida ti accompagna attraverso la progettazione e l'implementazione di un robusto pool di memoria a dimensione fissa in C. Imparerai a strutturare la piscina, gestire i casi di bordo come esaurimento e allineamento, e estendere il modello a scenari multi-pool.
Principi di progettazione di un pool di memoria
Un pool di memoria (chiamato anche un allocatore di lastre o un pool di oggetti) opera su una semplice idea: assegnare un grande blocco contiguo di memoria, dividerlo in “slots” a dimensione fissa e gestire quali slot sono liberi utilizzando un elenco a singola connessione. Quando un consumatore richiede memoria, la piscina restituisce la prima slot dalla lista gratuita. Quando una slot viene rilasciata, viene spinto indietro sulla testa della lista gratuita.
Piscine di dimensioni variabili
La variante più comune è la piscina a misura fissa, dove ogni slot è la stessa dimensione. Questo corrisponde all’oggetto che la piscina serve, ad esempio, una piscina di nodi . Le piscine a misura variabile (chiamate anche “arena allocators”) possono assegnare i detersivi di dimensioni diverse, ma devono gestire una lista gratuita di dimensioni variabili, gestire i casi di divisione e di carbonizzazione semplici, e evitare ancora il 90%
Considerazioni di allineamento
Se la tua piscina memorizza oggetti che contengono tipi come , , o vettori SIMD, la piscina deve garantire che ogni slot inizia ad un indirizzo allineato al più grande requisito di allineamento del tipo memorizzato. Lo standard C richiede per restituire la memoria adattabilmente allineata a qualsiasi tipo standard—che è,
Sicurezza del filo
Per applicazioni con un solo testo non è necessario sincronizzare, ma molti sistemi di produzione richiedono un accesso concomitante. L’aggiunta della sicurezza del thread a una piscina è semplice: proteggere la lista gratuita con un mutex, o utilizzare un elenco collegato senza blocco con para-e-swap atomico. Presenteremo la versione di base con un solo testo, ma discuteremo dei punti di estensione per ambienti multithreaded.
Costruire un pool di memoria fisso-dimensione: Passo dopo Passo
La piscina stessa è una struttura che tiene un puntatore alla memoria pre-allocata, una testa di lista gratuita, la dimensione dello slot (arrotondata per l'allineamento), e il numero totale di slot. La lista gratuita è una lista collegata incorporata ]inhead ogni slot libero: ogni slot libero memorizza un puntatore.
Strutture dati
#include <stddef.h>
#include <stdlib.h>
// Embedded free list node
typedef struct FreeNode {
struct FreeNode* next;
} FreeNode;
// Pool descriptor
typedef struct MemoryPool {
size_t slot_size; // Size of each slot (after alignment rounding)
size_t slot_count; // Number of slots in the pool
void* pool_start; // Start of the pre‑allocated memory block
FreeNode* free_list; // Head of the free list
} MemoryPool;
è utilizzato solo internamente; gli oggetti assegnati occupano la stessa memoria. Quando una slot è libera, i suoi primi byte contengono un puntatore successivo. Quando è assegnato, l'utente della piscina scrive i loro dati su quel puntatore. Ecco perché la dimensione dello slot deve essere almeno – in altro modo non possiamo memorizzare i puntatori di lista gratuiti.
Inizializzazione
L'inizializzazione alloca un singolo, grande blocco di memoria e collega ogni slot nella lista gratuita. Arrotondare la dimensione richiesta della slot al più vicino allineamento (che scegliamo come []]).
#include <stdint.h> // for max_align_t
int pool_init(MemoryPool* mp, size_t object_size, size_t object_count) {
// Round up object_size to the alignment of max_align_t
size_t alignment = _Alignof(max_align_t);
size_t aligned_size = (object_size + alignment - 1) & ~(alignment - 1);
// Ensure slot is large enough to hold a FreeNode pointer
if (aligned_size < sizeof(FreeNode))
aligned_size = sizeof(FreeNode);
mp->slot_size = aligned_size;
mp->slot_count = object_count;
// Allocate the contiguous pool memory
size_t total_size = aligned_size * object_count;
mp->pool_start = malloc(total_size);
if (mp->pool_start == NULL)
return -1; // allocation failure
// Build the free list
mp->free_list = (FreeNode*)mp->pool_start;
FreeNode* current = mp->free_list;
for (size_t i = 1; i < object_count; i++) {
current->next = (FreeNode*)((char*)mp->pool_start + i * aligned_size);
current = current->next;
}
current->next = NULL;
return 0;
}
Usiamo che garantisce l'allineamento più rigoroso richiesto da []. Per la maggior parte delle piattaforme questo è 8 o 16 byte. Il trucco di arrotondamento bitwise funziona per allineamento power-of-two.
Destinazione
Se la lista gratuita è vuota, la piscina è esaurita e ritorniamo .
void* pool_alloc(MemoryPool* mp) {
if (mp->free_list == NULL) {
return NULL; // pool exhausted
}
FreeNode* block = mp->free_list;
mp->free_list = block->next;
return (void*)block;
}
Questo è O(1) ed esegue in una manciata di istruzioni. Nessuna serratura, nessuna chiamata di sistema.
Liberare una Fessura
Il freeing spinge lo slot sulla lista gratuita. Il chiamante deve garantire che il puntatore appartiene a questa piscina (parleremo la convalida più tardi).
void pool_free(MemoryPool* mp, void* ptr) {
if (ptr == NULL) return; // standard behavior like free(NULL)
FreeNode* node = (FreeNode*)ptr;
node->next = mp->free_list;
mp->free_list = node;
}
Ancora O(1). Nessun carbonescing, nessuna fusione. La slot libera diventa immediatamente disponibile per il riutilizzo.
Destrutturazione della piscina
Quando la piscina non è più necessaria, liberare l'allocazione sottostante.
void pool_destroy(MemoryPool* mp) {
free(mp->pool_start);
mp->pool_start = NULL;
mp->free_list = NULL;
mp->slot_count = 0;
mp->slot_size = 0;
}
Chiamare sempre prima che la struttura della piscina esca dal campo di applicazione per evitare perdite di memoria.
Esempio di utilizzo
Ecco un esempio completo che crea una piscina di 1024 slot integer, alloca uno, scrive un valore, lo legge e lo libera.
#include <stdio.h>
#include <assert.h>
int main(void) {
MemoryPool pool;
if (pool_init(&pool, sizeof(int), 1024) != 0) {
fprintf(stderr, "Pool initialization failed\n");
return 1;
}
int* p = (int*)pool_alloc(&pool);
if (p == NULL) {
fprintf(stderr, "Pool exhausted\n");
return 1;
}
*p = 42;
printf("Value: %d\n", *p);
pool_free(&pool, p);
pool_destroy(&pool);
return 0;
}
In una vera applicazione, si alloca una piscina per ogni tipo di oggetto che è necessario gestire. Ad esempio, un server di rete potrebbe avere un e un .
Considerazioni e estensioni avanzate
Tracciamento di Allocation per Debugging
Per il debug, si potrebbe aggiungere un bitfield o un elenco separato di blocchi assegnati. Questo consente di rilevare doppie-free o perdite. In produzione, la testa di tracciamento è solitamente evitata—la natura deterministica delle piscine rende i bug più facili da trovare attraverso l'avvelenamento della memoria.
Avvelenamento della memoria
Quando una slot è liberata, è possibile sovrascrivere il suo contenuto con un modello noto (ad esempio, [) per rilevare l'uso-dopo-free. Allo stesso modo, quando si sta alleando, si potrebbe riempire lo slot con un modello per catturare le letture non inizializzate.
Statistiche di esportazione della piscina
Per la messa a punto delle prestazioni, esporre i contatori come le allocazioni totali, i totali liberi e il conteggio libero corrente. Un modo semplice è quello di mantenere un campo [ nella struttura della piscina, decrementing su alloc e incrementando gratuitamente.
// Add to MemoryPool: size_t free_count;
// In pool_alloc: if (mp->free_list) { mp->free_count--; ... }
// In pool_free: mp->free_count++; ...
Piscine di filo-salvata
Per l'accesso concomitante, avvolgere l'alloca e le funzioni libere con un mutex:
#include <pthread.h>
typedef struct ThreadSafePool {
MemoryPool pool;
pthread_mutex_t lock;
} ThreadSafePool;
void* ts_pool_alloc(ThreadSafePool* tsp) {
pthread_mutex_lock(&tsp->lock);
void* ptr = pool_alloc(&tsp->pool);
pthread_mutex_unlock(&tsp->lock);
return ptr;
}
void ts_pool_free(ThreadSafePool* tsp, void* ptr) {
pthread_mutex_lock(&tsp->lock);
pool_free(&tsp->pool, ptr);
pthread_mutex_unlock(&tsp->lock);
}
Per una minore soddisfazione, si consideri una lista gratuita senza blocco utilizzando []. Tuttavia, che richiede la gestione del problema ABA - una sfida classica descritta in molti libri di testo di convalutazione. Per la maggior parte delle applicazioni, le piscine per-thread sono più semplici e scalate meglio.
Crescere dinamicamente la piscina
Se avete bisogno di una piscina che può espandersi, potete mantenere una serie di pezzi di piscina. Quando un pezzo è esaurito, assegnare un nuovo pezzo (della stessa dimensione) e aggiungere le sue slot alla lista gratuita. L'allocatore rimane O(1) quasi sempre, ma dovete gestire più pezzi durante la distruzione.
Prestazioni Benchmarks (concettivo)
In un microbenchmark tipico su una CPU moderna x86‐64, un pool alloc/free cycle richiede 15–30 nanosecondi, mentre / per un oggetto di 32 byte può assumere 80–200 nanosecondi a causa di bloccaggio e metadati in testa.
Pitfalls comune e come evitare di loro
- Dimensioni di pool di miscelazione:[] Non liberare mai un puntatore che appartiene ad una piscina diversa (o a []) con ]. Il risultato è un comportamento non definito.
- Allignment mismatch:[] Se si memorizzano tipi con requisiti di allineamento insoliti (ad esempio, []), assicurarsi che il vostro allineamento delle slot è sufficiente. Il metodo copre tutti i tipi standard ma non può coprire i tipi SIMD.
- Permette di chiamare [: Il sottostante [ non è mai liberato se si salta la distruzione.
- Utilizzando la piscina per le assegnazioni di dimensioni variabili:[] Se avete bisogno di oggetti di dimensioni diverse, create piscine separate.
Contesto reale e lettura ulteriore
I pool di memoria personalizzati non sono una nuova idea, ma appaiono praticamente in ogni sistema ad alte prestazioni:
- Il kernel Linux utilizza slab allocators[] per le cache degli oggetti (vedere l'interfaccia ).
- I motori di gioco come Unreal Engine[ e Godot[] forniscono aglicatori di piscina incorporati per attori e particelle.
- Le librerie di rete (ad esempio, DPDK]) utilizzano i pool di memoria per i buffer di pacchetti per garantire l'assegnazione zero sul percorso veloce.
- La libreria Apache APR[]] include un'API di piscina utilizzata da Apache HTTP Server.
Per uno studio approfondito, leggi l’implementazione malloca della libreria GNU C[] per capire cosa stai evitando, e esaminare la documentazione kernel slab allocator[] per l’ispirazione progettuale. Il libro ]Programming with POSIX Threads]
Conclusioni
Gli assegnatori di memoria personalizzati sono un'ottimizzazione pratica e ad alto impatto per applicazioni che gestiscono molti oggetti di piccole e medie dimensioni. L'implementazione in C puro è piccola – inferiore a 50 linee di codice ben progettato – elimina la frammentazione, la mancanza di cache e la sovraccarico di allocatori di uso generale.