Loading learning content...
Every malloc() call you've ever made invokes one of the most critical pieces of systems software—the memory allocator. A well-designed allocator can make programs run 10x faster; a poorly designed one causes fragmentation, cache misses, and memory exhaustion.
Building your own allocator is a masterclass in systems programming: you'll manage raw memory, implement data structures without using malloc itself, handle alignment requirements, and optimize for real-world allocation patterns.
By the end of this page, you will understand heap management, implement first-fit/best-fit/next-fit strategies, handle coalescing and splitting, manage metadata, and understand the tradeoffs production allocators make.
An allocator manages a heap—a contiguous region of memory obtained from the OS. It must:
Memory is obtained from the OS via:
sbrk() — Extends the data segment (traditional Unix)mmap() — Maps anonymous memory pages (modern approach)The allocator subdivides these large regions into smaller blocks for application use.
| Goal | Description | Tradeoff |
|---|---|---|
| Throughput | Allocations per second | vs. memory efficiency |
| Utilization | Percentage of heap actually used | vs. speed |
| Locality | Related allocations near each other | vs. fragmentation |
| Low fragmentation | Minimize wasted space | vs. allocation speed |
Each block needs metadata to track its size and status. The challenge: this metadata consumes space and must be managed without using malloc!
Common approaches:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
#ifndef ALLOCATOR_H#define ALLOCATOR_H #include <stddef.h>#include <stdint.h> // Alignment requirement (typically 8 or 16 bytes)#define ALIGNMENT 16#define ALIGN(size) (((size) + (ALIGNMENT-1)) & ~(ALIGNMENT-1)) // Minimum block size (header + minimum payload)#define MIN_BLOCK_SIZE (sizeof(BlockHeader) + ALIGNMENT) /** * Block header - stored at the start of each block. * The size field uses the low bit as an "allocated" flag. */typedef struct BlockHeader { size_t size_and_flags; // Size + allocated bit struct BlockHeader *next; // Next free block (only used when free) struct BlockHeader *prev; // Previous free block (only used when free)} BlockHeader; // Extract size from header (mask off low bits)#define GET_SIZE(header) ((header)->size_and_flags & ~0xF) // Check if block is allocated#define IS_ALLOCATED(header) ((header)->size_and_flags & 0x1) // Set size and allocated flag#define SET_SIZE_ALLOC(header, size, alloc) \ ((header)->size_and_flags = ((size) & ~0xF) | ((alloc) & 0x1)) // Get footer from header (at end of block)#define GET_FOOTER(header) \ ((size_t *)((char *)(header) + GET_SIZE(header) - sizeof(size_t))) // Get next block in memory#define NEXT_BLOCK(header) \ ((BlockHeader *)((char *)(header) + GET_SIZE(header))) // Get previous block in memory (using footer)#define PREV_BLOCK(header) \ ((BlockHeader *)((char *)(header) - \ (*(((size_t *)(header)) - 1) & ~0xF))) // Convert header pointer to payload pointer#define HEADER_TO_PAYLOAD(header) \ ((void *)((char *)(header) + sizeof(BlockHeader))) // Convert payload pointer to header pointer#define PAYLOAD_TO_HEADER(payload) \ ((BlockHeader *)((char *)(payload) - sizeof(BlockHeader))) #endifmalloc() must return addresses aligned to the platform's requirement (typically 8 or 16 bytes). Misaligned access causes crashes on some architectures and performance penalties on others. Always round up block sizes to alignment boundaries.
The free list tracks available blocks. Different organizations optimize for different goals:
Implicit free list — Walk through all blocks, checking status
Explicit free list — Linked list of only free blocks
Segregated free lists — Multiple lists by size class
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
#include <unistd.h>#include <string.h>#include "allocator.h" // Global free list headstatic BlockHeader *free_list_head = NULL; // Heap boundariesstatic void *heap_start = NULL;static void *heap_end = NULL; /** * Initialize the allocator with initial heap. */void allocator_init(void) { // Request initial heap from OS heap_start = sbrk(0); void *request = sbrk(4096); // 4KB initial heap if (request == (void *)-1) { return; // sbrk failed } heap_end = sbrk(0); // Create initial free block spanning entire heap free_list_head = (BlockHeader *)heap_start; size_t heap_size = (char *)heap_end - (char *)heap_start; SET_SIZE_ALLOC(free_list_head, heap_size, 0); free_list_head->next = NULL; free_list_head->prev = NULL; // Set footer *GET_FOOTER(free_list_head) = heap_size;} /** * Add block to free list (LIFO insertion). */void add_to_free_list(BlockHeader *block) { block->next = free_list_head; block->prev = NULL; if (free_list_head != NULL) { free_list_head->prev = block; } free_list_head = block;} /** * Remove block from free list. */void remove_from_free_list(BlockHeader *block) { if (block->prev != NULL) { block->prev->next = block->next; } else { free_list_head = block->next; } if (block->next != NULL) { block->next->prev = block->prev; }} /** * First-fit: Find first block large enough. * Simple, fast, but causes fragmentation at heap start. */BlockHeader *find_first_fit(size_t size) { BlockHeader *current = free_list_head; while (current != NULL) { if (GET_SIZE(current) >= size) { return current; } current = current->next; } return NULL; // No suitable block found} /** * Best-fit: Find smallest block that fits. * Better utilization, but slower O(n) scan. */BlockHeader *find_best_fit(size_t size) { BlockHeader *current = free_list_head; BlockHeader *best = NULL; while (current != NULL) { size_t block_size = GET_SIZE(current); if (block_size >= size) { if (best == NULL || block_size < GET_SIZE(best)) { best = current; if (block_size == size) { break; // Perfect fit } } } current = current->next; } return best;}| Strategy | Time | Fragmentation | Use Case |
|---|---|---|---|
| First-fit | O(n) average | Moderate | General purpose |
| Next-fit | O(n) average | Better spread | Sequential allocs |
| Best-fit | O(n) always | Low internal | Memory-constrained |
| Worst-fit | O(n) always | High | Rarely used |
| Segregated | O(1) average | Low | Production allocators |
Now we implement the core allocation functions. Key operations:
malloc(size):
free(ptr):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
/** * Split a block if remainder is large enough. * Returns the block to allocate (may be original or split). */void split_block(BlockHeader *block, size_t needed_size) { size_t current_size = GET_SIZE(block); size_t remainder = current_size - needed_size; // Only split if remainder can hold minimum block if (remainder >= MIN_BLOCK_SIZE) { // Shrink current block SET_SIZE_ALLOC(block, needed_size, 1); *GET_FOOTER(block) = needed_size | 1; // Create new free block from remainder BlockHeader *new_block = NEXT_BLOCK(block); SET_SIZE_ALLOC(new_block, remainder, 0); *GET_FOOTER(new_block) = remainder; // Add new block to free list add_to_free_list(new_block); } else { // Use entire block (internal fragmentation) SET_SIZE_ALLOC(block, current_size, 1); *GET_FOOTER(block) = current_size | 1; }} /** * Extend heap when no suitable block exists. */BlockHeader *extend_heap(size_t size) { // Round up to page boundary for efficiency size_t extend_size = ALIGN(size); if (extend_size < 4096) extend_size = 4096; void *request = sbrk(extend_size); if (request == (void *)-1) { return NULL; // Out of memory } heap_end = sbrk(0); // Create new free block BlockHeader *new_block = (BlockHeader *)request; SET_SIZE_ALLOC(new_block, extend_size, 0); *GET_FOOTER(new_block) = extend_size; return new_block;} /** * malloc - Allocate memory of given size. */void *my_malloc(size_t size) { if (size == 0) return NULL; // Initialize on first call if (heap_start == NULL) { allocator_init(); } // Calculate required block size size_t block_size = ALIGN(size + sizeof(BlockHeader) + sizeof(size_t)); if (block_size < MIN_BLOCK_SIZE) { block_size = MIN_BLOCK_SIZE; } // Find suitable free block BlockHeader *block = find_first_fit(block_size); // Extend heap if necessary if (block == NULL) { block = extend_heap(block_size); if (block == NULL) { return NULL; // Out of memory } } else { remove_from_free_list(block); } // Split block if too large split_block(block, block_size); return HEADER_TO_PAYLOAD(block);} /** * Coalesce with adjacent free blocks. */BlockHeader *coalesce(BlockHeader *block) { size_t size = GET_SIZE(block); BlockHeader *next = NEXT_BLOCK(block); BlockHeader *prev = NULL; // Check if previous block exists and is free int prev_alloc = 1; // Assume allocated if ((void *)block > heap_start) { size_t *prev_footer = (size_t *)block - 1; prev_alloc = (*prev_footer) & 1; if (!prev_alloc) { prev = PREV_BLOCK(block); } } // Check if next block is free int next_alloc = 1; if ((void *)next < heap_end) { next_alloc = IS_ALLOCATED(next); } if (prev_alloc && next_alloc) { // No coalescing possible return block; } else if (prev_alloc && !next_alloc) { // Coalesce with next block remove_from_free_list(next); size += GET_SIZE(next); SET_SIZE_ALLOC(block, size, 0); *GET_FOOTER(block) = size; } else if (!prev_alloc && next_alloc) { // Coalesce with previous block remove_from_free_list(prev); size += GET_SIZE(prev); SET_SIZE_ALLOC(prev, size, 0); *GET_FOOTER(prev) = size; block = prev; } else { // Coalesce with both remove_from_free_list(prev); remove_from_free_list(next); size += GET_SIZE(prev) + GET_SIZE(next); SET_SIZE_ALLOC(prev, size, 0); *GET_FOOTER(prev) = size; block = prev; } return block;} /** * free - Return memory to the allocator. */void my_free(void *ptr) { if (ptr == NULL) return; BlockHeader *block = PAYLOAD_TO_HEADER(ptr); // Mark as free size_t size = GET_SIZE(block); SET_SIZE_ALLOC(block, size, 0); *GET_FOOTER(block) = size; // Coalesce adjacent free blocks block = coalesce(block); // Add to free list add_to_free_list(block);}Extend with: realloc() implementation, segregated free lists, thread-safe locking, memory debugging (detect double-free, use-after-free), and performance benchmarking against system malloc.