Skip to content

Instantly share code, notes, and snippets.

@apsun
Last active July 26, 2024 07:31
Show Gist options
  • Save apsun/caa3c5552dce7b13b898b70569b1f239 to your computer and use it in GitHub Desktop.
Save apsun/caa3c5552dce7b13b898b70569b1f239 to your computer and use it in GitHub Desktop.
A simple C memory allocator
/*
* myalloc - a simplified reimplementation of glibc's malloc
*
* This allocator aims to follow in the spirit of the glibc implementation,
* but with simplicity as the main design goal, instead of efficiency or
* scalability. It uses a single free list instead of grouping blocks
* into buckets, and is not at all thread safe.
*
* Some assumptions made:
* - 2's complement, little endian, 8 bits per byte
* - All pointers are 4 bytes on 32-bit systems, 8 bytes on 64-bit systems
* - sizeof(size_t) == sizeof(void *)
* - Pointers can be converted to/from size_t without loss of information
* - Maximum alignment requirement <= 2 * sizeof(size_t)
*
* Have fun, myaa~
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/*
* Whether we want to replace the C standard library functions.
*/
#ifndef MYA_REPLACE_STD
#define MYA_REPLACE_STD 1
#endif
#if MYA_REPLACE_STD
#define mya_malloc malloc
#define mya_calloc calloc
#define mya_realloc realloc
#define mya_free free
#endif
/*
* How much space the header info fields take up. This
* is effectively the minimum amount of overhead per allocation.
*/
#define MYA_INFO_SIZE (2 * sizeof(size_t))
/*
* The multiple by which to allocate user data blocks.
*/
#define MYA_DATA_ALIGN (2 * sizeof(size_t))
/*
* The multiple by which to increase the data break
* whenever we don't have enough space in the free list.
* Generally, this should be set to the page size for
* maximum performance.
*/
#define MYA_SBRK_ALIGN 4096
/*
* Masks for extracting the size and flags out of the
* header info fields.
*/
#define MYA_MASK_SIZE ~0x7
#define MYA_FLAG_USED 0x1
/*
* Rounds up to a multiple of the specified alignment.
* This may overflow if x is very large.
*/
#define mya_round_up(x, align) (((x) + (align) - 1) & -(align))
/*
* Macros to make type-casts more readable.
*/
#define mya_as_bytes(x) ((char *)(x))
#define mya_as_header(x) ((mya_header_t *)(x))
/*
* Macros for moving between the header and user data.
*/
#define mya_data_to_header(d) (mya_as_header(mya_as_bytes(d) - MYA_INFO_SIZE))
#define mya_header_to_data(h) (mya_as_bytes(h) + MYA_INFO_SIZE)
/*
* Macros for accessing the header size and flags independently.
*
* w = which info field to access, either 'prev' or 'curr'
* h = pointer to header
* v = new value
*/
#define mya_info(w, h) (h->w##_info)
#define mya_size(w, h) (mya_info(w, h) & MYA_MASK_SIZE)
#define mya_used(w, h) (mya_info(w, h) & MYA_FLAG_USED)
#define mya_set_size(w, h, v) (mya_info(w, h) = (mya_info(w, h) & ~MYA_MASK_SIZE) | (v))
#define mya_set_used(w, h, v) (mya_info(w, h) = (mya_info(w, h) & ~MYA_FLAG_USED) | (v))
#define mya_is_sentinel(w, h) (mya_size(w, h) == 0)
/*
* Macros for moving to the previous and next adjacent block headers.
*/
#define mya_next(h) (mya_as_header(mya_header_to_data(h) + mya_size(curr, h)))
#define mya_prev(h) (mya_data_to_header(mya_as_bytes(h) - mya_size(prev, h)))
/*
* Block header structure. Contains allocation metadata.
* If the current block is allocated, prev_free and next_free are invalid.
*
* | . | |
* | . | | user data
* _|_____________|_|
* | | prev_info |
* | | curr_info |_
* header | | prev_free | |
* |_|__next_free__| |
* | . | |
* | . | | user data
* _|_____________|_|
* | | prev_info |
* | | curr_info |_
* header | | prev_free | |
* |_|__next_free__| |
* | . | | user data
* | . | |
*/
typedef struct mya_header {
/*
* Holds the user data size in bytes and status flags of
* the previous adjacent block.
*/
size_t prev_info;
/*
* Holds the user data size in bytes and status flags of
* the current block.
*/
size_t curr_info;
/*
* If the current block is not in use, holds a pointer to
* the previous block in the free list.
*/
struct mya_header *prev_free;
/*
* If the current block is not in use, holds a pointer to
* the next block in the free list.
*/
struct mya_header *next_free;
} mya_header_t;
/*
* Doubly linked list of free blocks. If there are no free blocks,
* this will be NULL.
*/
static mya_header_t *mya_free_list = NULL;
/*
* This caches the value of the last call to sbrk, so that we can
* efficiently check if an allocation would overflow.
*/
static void *mya_last_brk = NULL;
/*
* Whether we've initialized the global state.
*/
static bool mya_initialized = false;
/*
* Adds a block to the free list. Upon entry,
* prev_free and next_free may be in an invalid state.
*/
static void
mya_add_free_list(mya_header_t *header)
{
header->prev_free = NULL;
header->next_free = mya_free_list;
if (mya_free_list != NULL) {
mya_free_list->prev_free = header;
}
mya_free_list = header;
}
/*
* Removes a block from the free list. Upon exit,
* prev_free and next_free may be in an invalid state.
*/
static void
mya_remove_free_list(mya_header_t *header)
{
mya_header_t *prev_free = header->prev_free;
mya_header_t *next_free = header->next_free;
if (mya_free_list == header) {
mya_free_list = next_free;
}
if (prev_free != NULL) {
prev_free->next_free = next_free;
}
if (next_free != NULL) {
next_free->prev_free = prev_free;
}
}
/*
* Attempts to coalesce a block with the next adjacent block.
* This will never invalidate the original block. The block
* may be allocated. Returns whether the block was coalesced.
*/
static bool
mya_coalesce_next(mya_header_t *header)
{
/* Can't coalesce if the next adjacent block is allocated */
mya_header_t *next_adj = mya_next(header);
if (mya_used(curr, next_adj)) {
return false;
}
/* Remove next adjacent block from free list */
mya_remove_free_list(next_adj);
/* New size equals combined size of two blocks plus header overhead */
size_t new_size = mya_size(curr, header) + MYA_INFO_SIZE + mya_size(curr, next_adj);
/* Update previous size and usage flag in next next adjacent block */
mya_header_t *next_next_adj = mya_next(next_adj);
mya_set_used(prev, next_next_adj, mya_used(curr, header));
mya_set_size(prev, next_next_adj, new_size);
/* Update size of the current block */
mya_set_size(curr, header, new_size);
return true;
}
/*
* Attempts to coalesce a free block with the previous adjacent block.
* If coalescing occurs, the original block will be invalidated.
*/
static mya_header_t *
mya_coalesce_prev(mya_header_t *header)
{
if (!mya_used(prev, header)) {
header = mya_prev(header);
mya_coalesce_next(header);
}
return header;
}
/*
* Attempts to coalesce a free block in both directions.
* Returns a pointer to the new, possibly merged free block.
* The original block will be invalidated if backwards
* coalescing occurs.
*/
static mya_header_t *
mya_coalesce(mya_header_t *header)
{
/* Coalesce forwards */
mya_coalesce_next(header);
/* Coalesce backwards */
return mya_coalesce_prev(header);
}
/*
* Wrapper for sbrk that checks for overflow.
* Note that delta is unsigned, since we don't support
* shrinking the data break on free yet.
*/
static bool
mya_sbrk(size_t delta, void **orig_brk, void **new_brk)
{
/* If allocation would overflow, fail fast */
if ((size_t)mya_last_brk + delta < (size_t)mya_last_brk) {
return false;
}
/* We know the allocation is safe, call sbrk */
void *last_brk;
if ((last_brk = sbrk(delta)) == (void *)-1) {
return false;
}
/* Update cached brk value */
mya_last_brk = (void *)((char *)last_brk + delta);
*orig_brk = last_brk;
*new_brk = mya_last_brk;
return true;
}
/*
* Initializes the global allocator state. After this function
* returns true, it must not be called again.
*/
static bool
mya_initialize(void)
{
/*
* This function sets up the sentinel headers, one at the
* start of the data, one at the end. Notice that the sentinel
* at the end is only halfway allocated; only the info fields
* lie within a valid page.
* ____________________________________________
* | | size = 0 | used = 1 | ^
* | | size = X | used = 0 |___ |
* header | | prev_free = NULL | ^ |
* |_|___next_free = NULL__| | |
* | . | X MYA_SBRK_ALIGN
* | . | | |
* _|_____________________|_v_ |
* | | size = X | used = 0 | |
* header | | size = 0 | used = 1 |____________v_______
*/
/* Initialize cached brk value */
mya_last_brk = sbrk(0);
if (mya_last_brk == (void *)-1) {
return false;
}
/* Allocate some starting memory */
void *orig_brk, *new_brk;
if (!mya_sbrk(MYA_SBRK_ALIGN, &orig_brk, &new_brk)) {
return false;
}
/* Set up the sentinel blocks */
mya_header_t *bottom = mya_as_header(orig_brk);
mya_set_size(prev, bottom, 0);
mya_set_used(prev, bottom, 1);
mya_header_t *top = mya_data_to_header(new_brk);
mya_set_size(curr, top, 0);
mya_set_used(curr, top, 1);
/* Initialize the initial free block */
mya_set_size(curr, bottom, MYA_SBRK_ALIGN - 2 * MYA_INFO_SIZE);
mya_set_used(curr, bottom, 0);
mya_set_size(prev, top, MYA_SBRK_ALIGN - 2 * MYA_INFO_SIZE);
mya_set_used(prev, top, 0);
/* Add free block to free list */
mya_add_free_list(bottom);
mya_initialized = true;
return true;
}
/*
* Finds a block that is large enough to fit an allocation
* with the specified user data size, or NULL if there are
* no available blocks that are large enough.
*/
static mya_header_t *
mya_find_free_block(size_t aligned_size)
{
mya_header_t *header = mya_free_list;
while (header != NULL) {
if (mya_size(curr, header) >= aligned_size) {
return header;
}
header = header->next_free;
}
return NULL;
}
/*
* Allocates a new block from the system with at least the specified
* size. If there is no more available memory, returns NULL. Otherwise,
* returns the header of the top-most block (which might not be the
* new block due to coalescing, if the original top-most block was free).
*/
static mya_header_t *
mya_sbrk_new_block(size_t aligned_size)
{
/* Round to a multiple of the page size */
size_t page_size = mya_round_up(aligned_size + MYA_INFO_SIZE, MYA_SBRK_ALIGN);
/* Request some more memory from the kernel */
void *orig_brk, *new_brk;
if (!mya_sbrk(page_size, &orig_brk, &new_brk)) {
return NULL;
}
/* New block starts right before the data break */
mya_header_t *header = mya_data_to_header(orig_brk);
/* Convert sentinel to a normal block */
mya_set_size(curr, header, page_size - MYA_INFO_SIZE);
mya_set_used(curr, header, 0);
/* Initialize new sentinel block */
mya_header_t *sentinel = mya_data_to_header(new_brk);
mya_set_size(prev, sentinel, page_size - MYA_INFO_SIZE);
mya_set_used(prev, sentinel, 0);
mya_set_size(curr, sentinel, 0);
mya_set_used(curr, sentinel, 1);
/* Add new block to free list */
mya_add_free_list(header);
/* Coalesce with the previous block if it's free */
return mya_coalesce_prev(header);
}
/*
* Attempts to split a block into two smaller blocks, with the first
* having size >= aligned_size. If the block is too small, returns NULL.
* Otherwise, returns a pointer to the second block, which will be unused.
* The second block may be coalesced with the next adjacent block.
*/
static mya_header_t *
mya_split_block(mya_header_t *header, size_t aligned_size)
{
size_t curr_size = mya_size(curr, header);
/* Only split if we have enough space for another allocation */
if (curr_size < aligned_size + MYA_INFO_SIZE + MYA_DATA_ALIGN) {
return NULL;
}
/* This will be the size of our split block */
size_t split_size = curr_size - aligned_size - MYA_INFO_SIZE;
/* Update the previous field of the next adjacent block */
mya_header_t *next_header = mya_next(header);
mya_set_size(prev, next_header, split_size);
mya_set_used(prev, next_header, 0);
/* Update the original header with the new size */
mya_set_size(curr, header, aligned_size);
/* Now find out where our split header is */
mya_header_t *split_header = mya_next(header);
/* Initialize prev field of split header */
mya_set_size(prev, split_header, aligned_size);
mya_set_used(prev, split_header, mya_used(curr, header));
/* Initialize curr field of split header */
mya_set_size(curr, split_header, split_size);
mya_set_used(curr, split_header, 0);
/* Add new block to free list */
mya_add_free_list(split_header);
/* Try to coalesce new block with the next adjacent block */
mya_coalesce_next(split_header);
return split_header;
}
/*
* Allocates the specified number of bytes and returns a pointer
* to the allocated memory. If size equals 0 or there is no
* more memory available, NULL is returned.
*/
void *
mya_malloc(size_t size)
{
/* malloc(0) always returns NULL */
if (size == 0) {
return NULL;
}
/* Initialize global state on first run */
if (!mya_initialized && !mya_initialize()) {
return NULL;
}
/* Round allocation size up to an appropriate alignment */
size_t aligned_size = mya_round_up(size, MYA_DATA_ALIGN);
if (aligned_size == 0) {
return NULL;
}
/*
* First try to find an existing free block.
* If that fails, try to allocate a new block.
* If that also fails, we've run out of memory.
*/
mya_header_t *header = mya_find_free_block(aligned_size);
if (header == NULL) {
header = mya_sbrk_new_block(aligned_size);
if (header == NULL) {
return NULL;
}
}
/* Split the block as necessary */
mya_split_block(header, aligned_size);
/* Remove block from the free list */
mya_remove_free_list(header);
/* Mark block as allocated */
mya_set_used(curr, header, 1);
mya_set_used(prev, mya_next(header), 1);
/* Return pointer to the user data */
return mya_header_to_data(header);
}
/*
* Frees a block of memory originally allocated by malloc,
* calloc, or realloc. Calling free on NULL is a no-op.
*/
void
mya_free(void *ptr)
{
/* free(NULL) is a no-op */
if (ptr == NULL) {
return;
}
/* Find header for the user data */
mya_header_t *header = mya_data_to_header(ptr);
/* Mark block as free */
mya_set_used(curr, header, 0);
mya_set_used(prev, mya_next(header), 0);
/* Add block into the free list */
mya_add_free_list(header);
/* Coalesce with any neighboring free blocks */
mya_coalesce(header);
}
/*
* Allocates a 0-initialized block of memory of with size
* equal to num * size. If num * size would overflow, or
* num and/or size equal 0, or there is no more available
* memory, NULL is returned.
*/
void *
mya_calloc(size_t num, size_t size)
{
/* calloc(num, 0) and calloc(0, size) always returns NULL */
if (size == 0 || num == 0) {
return NULL;
}
/* Prevent overflow when computing num * size */
if (num > SIZE_MAX / size) {
return NULL;
}
/* Otherwise, simply use malloc() followed by memset() */
void *ptr = mya_malloc(num * size);
if (ptr != NULL) {
memset(ptr, 0, num * size);
}
return ptr;
}
/*
* Changes the size of a previously allocated memory block.
* The contents will be unchanged up to the previous size of
* the block, and any additional memory will not be initialized.
* If size equals 0, this is equivalent to calling free(ptr). If
* ptr is NULL, this is equivalent to calling malloc(size).
*/
void *
mya_realloc(void *ptr, size_t size)
{
/* realloc(NULL, size) is the same as malloc(size) */
if (ptr == NULL) {
return mya_malloc(size);
}
/* realloc(ptr, 0) is the same as free(ptr) */
if (size == 0) {
free(ptr);
return NULL;
}
/* Round allocation size up to an appropriate alignment */
size_t aligned_size = mya_round_up(size, MYA_DATA_ALIGN);
/* Find header for the user data */
mya_header_t *header = mya_data_to_header(ptr);
/* Save original size of block */
size_t orig_size = mya_size(curr, header);
/*
* If we're shrinking the block, try to split it
* and return the same block. No copying required.
*/
if (aligned_size <= orig_size) {
mya_split_block(header, aligned_size);
return ptr;
}
/* Try coalescing with the next block */
if (mya_coalesce_next(header)) {
if (aligned_size <= mya_size(curr, header)) {
mya_split_block(header, aligned_size);
return ptr;
}
}
/* If this is the last block, just sbrk some more memory */
if (mya_is_sentinel(curr, mya_next(header))) {
size_t sbrk_size = aligned_size - mya_size(curr, header);
mya_header_t *next_alloc = mya_sbrk_new_block(sbrk_size);
if (next_alloc != NULL) {
mya_coalesce_next(header);
mya_split_block(header, aligned_size);
return ptr;
}
}
/*
* We tried everything but we still can't resize it in-place,
* fall back to malloc() followed by memcpy().
*/
void *new_ptr = mya_malloc(size);
if (new_ptr != NULL) {
memcpy(new_ptr, ptr, orig_size);
mya_free(ptr);
}
return new_ptr;
}
/*
* Some basic allocation correctness tests.
*/
#include <assert.h>
#include <stdio.h>
#include <time.h>
#include <signal.h>
#define SMALL_SIZE_MIN 0
#define SMALL_SIZE_MAX 64
#define LARGE_SIZE_MIN 512
#define LARGE_SIZE_MAX 1000000
#define ITERATION_COUNT 10000
#define RAND_RANGE(a, b) ((a) + rand() % ((b) - (a)))
#define RAND_SIZE() \
((rand() & 1) \
? RAND_RANGE(SMALL_SIZE_MIN, SMALL_SIZE_MAX) \
: RAND_RANGE(LARGE_SIZE_MIN, LARGE_SIZE_MAX))
/* Saves the rand seed so we can reproduce crashes */
static unsigned int seed;
static void
handle_segfault(int signum)
{
(void)signum;
printf("FAIL! Seed: %u\n", seed);
exit(1);
}
int
main(void)
{
#if MYA_REPLACE_STD
printf("Using custom allocator functions\n");
#else
printf("Using stdlib allocator functions\n");
#endif
/* 0-sized allocation checks */
(void)malloc(0);
void *_ = realloc(NULL, 0);
(void)_;
(void)calloc(1, 0);
/* Overflow checks */
assert(malloc(SIZE_MAX) == NULL);
assert(realloc(NULL, SIZE_MAX) == NULL);
assert(calloc(1, SIZE_MAX) == NULL);
assert(calloc(SIZE_MAX, SIZE_MAX) == NULL);
/* Stuff to store our checks */
char *ptrs[ITERATION_COUNT];
int sizes[ITERATION_COUNT];
char chrs[ITERATION_COUNT];
/* Handle segfaults for debugging */
struct sigaction sa;
sa.sa_handler = handle_segfault;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
/* I can haz randomness? */
seed = (unsigned int)time(NULL);
srand(seed);
/* Start benchmark */
clock_t start = clock();
/* malloc some randomly sized blocks */
for (int i = 0; i < ITERATION_COUNT; ++i) {
size_t sz = RAND_SIZE();
sizes[i] = 0;
ptrs[i] = malloc(sz);
if (ptrs[i] != NULL) {
sizes[i] = sz;
chrs[i] = (char)RAND_RANGE(0, 256);
for (size_t j = 0; j < sz; ++j) {
ptrs[i][j] = chrs[i];
}
}
}
/* free some of the pointers */
for (int i = 0; i < ITERATION_COUNT / 2; ++i) {
int index = rand() % ITERATION_COUNT;
free(ptrs[index]);
ptrs[index] = NULL;
sizes[index] = 0;
chrs[index] = '\0';
}
/* realloc some of the pointers */
for (int i = 0; i < ITERATION_COUNT / 2; ++i) {
int index = rand() % ITERATION_COUNT;
size_t sz = RAND_SIZE();
void *x = realloc(ptrs[index], sz);
if (sz == 0 || x != NULL) {
ptrs[index] = x;
sizes[index] = sz;
chrs[index] = (char)RAND_RANGE(0, 256);
for (size_t j = 0; j < sz; ++j) {
ptrs[index][j] = chrs[index];
}
}
}
/* Make sure our data is still intact */
for (int i = 0; i < ITERATION_COUNT; ++i) {
for (int j = 0; j < sizes[i]; ++j) {
assert(ptrs[i][j] == chrs[i]);
}
}
/* Clean up our mess */
for (int i = 0; i < ITERATION_COUNT; ++i) {
free(ptrs[i]);
ptrs[i] = NULL;
sizes[i] = 0;
chrs[i] = '\0';
}
/* End benchmark */
clock_t stop = clock();
double total_time = (double)(stop - start) / CLOCKS_PER_SEC;
printf("PASS! Time: %.02fs\n", total_time);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment