Dynamic Memory Allocator in C

This project focuses on building custom implementations of malloc(), calloc(), free(), and realloc() in C. It optimizes memory management by reducing fragmentation and enhancing performance, offering deeper insight into low-level memory handling and C programming.
Project image

Key Skills Acquired

Before diving into the project, it's essential to highlight some of the key skills and concepts that were central to this endeavor:

  • Dynamic Memory Allocation: Implementing and understanding malloc(), calloc(), free(), and realloc() functions in C.
  • Memory Optimization: Reducing fragmentation and improving allocation efficiency through various optimization techniques.
  • C Programming: Mastering low-level memory management in C, including pointer arithmetic and memory block management.
  • System-Level Programming: Gaining a deeper understanding of how the operating system interacts with dynamic memory allocation.

The Goal: Recreating and Optimizing Core Memory Functions

Dynamic memory allocation is a cornerstone of system-level programming in C. The standard library provides several key functions—malloc(), calloc(), free(), and realloc()—to manage memory at runtime. However, these functions are often treated as black boxes by most developers. My goal was to recreate these functions from scratch, implementing them with a focus on performance and efficiency.

Understanding the Basics

malloc()

The malloc() function allocates a specified amount of memory and returns a pointer to the beginning of the block. The memory is uninitialized, meaning it contains garbage data.

calloc()

Unlike malloc(), calloc() allocates memory for an array of elements and initializes all bytes to zero. This is particularly useful when you need a clean memory slate.

free()

The free() function deallocates previously allocated memory, making it available for future use. Properly implementing free() is crucial to avoid memory leaks and ensure efficient memory reuse.

realloc()

The realloc() function adjusts the size of a previously allocated memory block. It’s a versatile function that either expands or shrinks the memory while preserving the data.

The Implementation: Recreating the Core Functions

Building the Foundation

The first step was to set up a memory pool that our custom allocator could manage. I used the sbrk() system call to expand the program's data space, creating a large, contiguous block of memory to work with.

void *memory_pool = sbrk(POOL_SIZE);
if (memory_pool == (void*) -1) {
    // Handle error
}

Implementing malloc()

The custom malloc() function needed to find a free block of memory within our pool. I implemented a first-fit strategy to locate the first block large enough to satisfy the allocation request.

void *my_malloc(size_t size) {
    Block *curr = free_list;
    while (curr) {
        if (curr->free && curr->size >= size) {
            curr->free = 0;
            // Split block if necessary
            return (void*)(curr + 1);
        }
        curr = curr->next;
    }
    // Allocate more memory if no suitable block is found
}

Implementing calloc()

For calloc(), I built upon the malloc() function but added an initialization step to zero out the allocated memory.

void *my_calloc(size_t num, size_t size) {
    size_t total_size = num * size;
    void *ptr = my_malloc(total_size);
    if (ptr) {
        memset(ptr, 0, total_size);
    }
    return ptr;
}

Implementing free()

The free() function marked a block as free and attempted to merge adjacent free blocks to reduce fragmentation.

void my_free(void *ptr) {
    if (!ptr) return;
    
    Block *block = (Block*)ptr - 1;
    block->free = 1;
    
    // Coalesce adjacent free blocks
    coalesce_free_blocks(block);
}

Implementing realloc()

The realloc() function presented a unique challenge, as it needed to adjust the size of a block while preserving its content. If the block could not be resized in place, the function would allocate a new block, copy the data, and free the old block.

void *my_realloc(void *ptr, size_t size) {
    if (!ptr) return my_malloc(size);
    if (size == 0) {
        my_free(ptr);
        return NULL;
    }
    
    Block *block = (Block*)ptr - 1;
    if (block->size >= size) {
        // Resize in place
        return ptr;
    } else {
        // Allocate new block
        void *new_ptr = my_malloc(size);
        if (new_ptr) {
            memcpy(new_ptr, ptr, block->size);
            my_free(ptr);
        }
        return new_ptr;
    }
}

Optimizing Memory Allocation

Fragmentation Reduction

One of the significant challenges with dynamic memory allocation is fragmentation, where small, unused memory blocks are scattered throughout the pool. To combat this, I implemented block coalescing in the free() function, which merged adjacent free blocks into a single larger block.

Efficient Block Splitting

When a large block is split into a smaller block during allocation, the remainder of the block could be wasted. To avoid this, I ensured that any leftover memory after a split was added back to the free list, making it available for future allocations.

Quick-Fit Arrays

For common allocation sizes, I implemented a quick-fit array. This array pre-allocated blocks of specific sizes, reducing the time needed to find a suitable block for these frequent requests.

Conclusion: A Deeper Understanding of Memory Management

Recreating malloc(), calloc(), free(), and realloc() was a challenging and insightful project. It required a deep dive into how memory is managed at the system level and forced me to think critically about optimization techniques. The experience has not only enhanced my understanding of dynamic memory allocation but also improved my overall C programming skills.

This project serves as a reminder that sometimes, the best way to understand how something works is to build it yourself. Through this process, I gained valuable insights into the inner workings of memory management in C and developed a more profound respect for the complexities involved.