DEV Community

Cover image for Writing Your Own Memory Pool Allocator in C
Trish
Trish

Posted on

Writing Your Own Memory Pool Allocator in C

Writing Your Own Memory Pool Allocator in C: A Step-by-Step Guide

In C, dynamic memory management is a crucial aspect of developing efficient software, particularly in performance-critical applications. While functions like malloc() and free() in the standard library are commonly used, they come with overhead and limitations, such as fragmentation and slower allocation times when called frequently. One solution to these issues is creating a memory pool allocator.

In this blog, we will walk through how to write a simple memory pool allocator from scratch in C. By using a memory pool, we can pre-allocate a large block of memory and manage it manually, reducing fragmentation and improving memory allocation performance.

🔗 Keep the conversation going on Twitter(X): @trish_07

What is a Memory Pool Allocator?

A memory pool allocator is a custom memory management strategy where a large block of memory is pre-allocated, and smaller chunks of it are handed out to the program as needed. When memory is no longer needed, it is returned to the pool for reuse. This approach allows for faster allocation and deallocation than using malloc() and free() directly, as well as better memory utilization.

Here’s how a basic memory pool works:

  • Pre-allocate a large block of memory.
  • Divide this block into smaller chunks (blocks).
  • Keep track of the unused blocks in a free list.
  • When a block is requested, allocate it from the pool and return it to the caller.
  • When a block is freed, return it to the pool.

Step 1: Define the Memory Pool Structure

We will begin by defining a simple structure for the memory pool and the blocks within it. Each block will have a pointer to the next block in the free list, which allows us to quickly allocate and free memory.

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>

#define POOL_SIZE 1024  // Total memory pool size

// Define a block structure with a pointer to the next free block
typedef struct Block {
    struct Block *next;
} Block;

// Define the MemoryPool structure
typedef struct {
    Block *freeList;
    unsigned char pool[POOL_SIZE]; // Pre-allocated pool
} MemoryPool;
Enter fullscreen mode Exit fullscreen mode

In this code:

  • POOL_SIZE is the total size of the memory pool. We will allocate a static array to simulate the pool.
  • The Block structure represents a single chunk of memory, and it includes a pointer (next) that links it to the next block in the free list.
  • The MemoryPool structure contains the freeList pointer (which tracks free blocks) and a pool array that holds the actual pre-allocated memory.

Step 2: Initialize the Memory Pool

To initialize the memory pool, we need to divide the pool into blocks and set up the free list. Each block should point to the next free block.

void initMemoryPool(MemoryPool *pool) {
    pool->freeList = (Block *)pool->pool;
    Block *current = pool->freeList;

    // Create a free list of blocks
    for (int i = 0; i < (POOL_SIZE / sizeof(Block)) - 1; i++) {
        current->next = (Block *)((unsigned char *)current + sizeof(Block));
        current = current->next;
    }

    current->next = NULL; // Last block points to NULL
}
Enter fullscreen mode Exit fullscreen mode

In this function:

  • We initialize the freeList to point to the beginning of the pool.
  • We then loop through the pool, setting the next pointer of each block to the next one in memory.
  • Finally, the last block points to NULL to indicate the end of the free list.

Step 3: Allocating Memory from the Pool

To allocate memory, we need to get the first available block from the free list. Once we allocate a block, we remove it from the free list.

void *allocateMemory(MemoryPool *pool) {
    if (pool->freeList == NULL) {
        printf("Memory pool exhausted!\n");
        return NULL;
    }

    // Get the first free block
    Block *block = pool->freeList;
    pool->freeList = block->next; // Move the free list pointer

    return (void *)block;
}
Enter fullscreen mode Exit fullscreen mode

This function checks if the free list is empty. If not, it takes the first free block, removes it from the free list, and returns it to the caller.

Step 4: Freeing Memory and Adding it Back to the Pool

When memory is freed, we return the block to the free list. This allows it to be reused for future allocations.

void freeMemory(MemoryPool *pool, void *ptr) {
    Block *block = (Block *)ptr;
    block->next = pool->freeList; // Add the block to the free list
    pool->freeList = block;
}
Enter fullscreen mode Exit fullscreen mode

Here, we add the freed block to the front of the free list by setting its next pointer to the current first block in the free list. This allows the block to be reused in the future.

Step 5: Example Usage

Now that we have all the necessary functions, let’s put everything together and test our memory pool allocator.

int main() {
    MemoryPool pool;
    initMemoryPool(&pool); // Initialize the pool

    // Allocate a few blocks
    void *block1 = allocateMemory(&pool);
    void *block2 = allocateMemory(&pool);
    printf("Allocated block1: %p\n", block1);
    printf("Allocated block2: %p\n", block2);

    // Free the blocks
    freeMemory(&pool, block1);
    freeMemory(&pool, block2);
    printf("Freed block1 and block2\n");

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • We initialize the memory pool using initMemoryPool().
  • We then allocate two blocks using allocateMemory().
  • Finally, we free the blocks using freeMemory().

When you run this program, you should see output similar to this:

Allocated block1: 0x55f7f6f34a80
Allocated block2: 0x55f7f6f34a88
Freed block1 and block2
Enter fullscreen mode Exit fullscreen mode

Why Use a Memory Pool?

  1. Performance: Memory pools are typically faster than repeatedly calling malloc() and free() because the overhead of system-level memory management is minimized.
  2. Avoid Fragmentation: Memory pools help avoid fragmentation by allocating fixed-size blocks of memory.
  3. Predictability: Memory allocation becomes predictable since the program controls the allocation and deallocation.

Memory pools are especially useful in real-time systems, embedded systems, and games, where low-latency and memory efficiency are critical.

Conclusion

Writing your own memory pool allocator can significantly optimize memory management for performance-critical applications. By managing memory directly, you can improve allocation speed, reduce fragmentation, and gain more control over how memory is used in your program. While this example is basic, you can extend it with additional features such as different block sizes or thread-safe memory allocation.

If you’re working on a project that requires efficient memory management, consider implementing your own memory pool. It’s a great way to dive deeper into memory management and improve the performance of your application.


Feel free to reach out if you have any questions or need further clarification. Happy coding! 🚀

Top comments (0)