Simple memory pool implementation

Why use memory pools

Avoid memory fragmentation
Reduce the number of system calls (brk, mmap)

Memory fragmentation

What is it?

Unavailable free memory. These memories are small and discontinuous

Cause

The starting address of memory allocation needs to be a multiple of 4 / 8 / 16 (determined by CPU architecture)
Small pieces of free memory sandwiched between large pieces of memory are not easy to use

Possible problems
  • The server program will become unresponsive after a long cycle or a large number of accesses. After investigating the reasons, it is found that the occupied memory will increase irregularly and abnormally with the increase of the number of requests. If you use valgrind and other tools to find that there is no memory leakage, it may be memory fragments.

  • Memory problems occur when the program runs for several days or even months: for example, malloc returns NULL.

How

Using the open source solution jemalloc / tcmalloc
Implement your own memory pool

Usage scenario

Global memory pool

  • The life cycle is when small memory needs to be reclaimed during program operation
  • You can use jemalloc / tcmalloc directly

1 connection 1 memory pool

  • Short life cycle, small memory does not need to be recycled
  • If a connection has only one thread, it does not need to be locked

Issues to consider in designing memory pools

  • Slow search speed
  • Small memory recycling trouble
  • In order to avoid memory fragmentation caused by small memory blocks, the structure definition occupies memory and memory blocks need to be a continuous block of memory.

Memory layout

Code flow chart

code implementation

Following nginx, this paper implements a memory pool based on the scheme of one connection and one memory pool

  • Block definition
    small_trunk
  • Block definition
    big_trunk
  • Memory pool definition
    mempool
  • Create memory pool
    mempool_create
  • Destroy memory pool
    mempool_destroy
  • Reset memory pool
    mempool_reset
  • Block allocation
    mempool_alloc_trunk
  • Block allocation
    mempool_alloc_big
  • Block allocation
    mempool_alloc_small
  • Freeing large blocks of memory
    mempool_free_trunk

Tail insertion method for small linked list
Head insertion method for large linked list
Large chunks of nodes (excluding data) are located in small chunks of memory
The size of the small memory is determined by the user: mempool_create(size_t size)

Specific code

/*
 * Memory pool structure definition
 * Create memory pool
 * Block allocation
 * Block allocation
 * Block allocation
 * Bulk release
 * Destroy memory pool
 * Reset memory pool
*/

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

#define ENABLE_LOG

#define MP_ALIGNMENT        32
#define MP_MAX_SMALL_TRUNK  4096
#define MP_ MIN_ SMALL_ Trunk 16 / / guaranteed big_ The trunk structure is a small block of memory

#define ALIGN_PTR(ptr, align)      (void*)(((size_t)(ptr) + ((align) - 1)) & (~((align) - 1)))

/*
 * If the pointer is defined as void *, if an arithmetic operation (+ or -) is to be performed, the unit is 1, which is the same as the unsigned char *
 * 6.24 Arithmetic on void- and Function-PointersIn GNU C, 
 * addition and subtraction operations are supported on pointers to void and on pointers to functions. 
 * This is done by treating the size of a void or of a function as 1.
 * A consequence of this is that sizeof is also allowed on void and on function types, and returns 1.
 * The option -Wpointer-arith requests a warning if these extensions are used.
 * 
 * It is defined as unsigned char * to know more clearly that the unit is 1
 * 
 * Note that the pointer is a structure pointer, which should be converted to unsigned char for arithmetic operation*
*/

// Small pieces
typedef struct _small_trunk {
    unsigned char*          last;               // Unused memory header address
    unsigned char*          end;                // Unused suffix address
    struct _small_trunk*    next;               // Point to the next piece
    size_t                  failed;             // Number of application failures
} small_trunk;  

// Chunks
typedef struct _big_trunk {
    struct _big_trunk*  next;               // Point to next block
    void*               start;              // Block header address
} big_trunk;

// Memory pool
typedef struct _mempool {
    size_t          border;          // Size block boundary 
    small_trunk*    cur_small;       // Current block
    big_trunk*      big;             // First block
    small_trunk     first_small[0];  // Point to the first block
} mempool;

void* mempool_alloc_trunk(mempool* pool, size_t size);

mempool* mempool_create(size_t size) {
    mempool* pool;
    int ret = posix_memalign((void**)&pool, MP_ALIGNMENT, sizeof(mempool) + sizeof(small_trunk) + size);
    if (0 != ret) {
        return NULL;
    }
    memset(pool, 0, sizeof(mempool) + sizeof(small_trunk) + size);

    pool->border = size > MP_MAX_SMALL_TRUNK ? MP_MAX_SMALL_TRUNK : size;
    pool->cur_small = pool->first_small;
    pool->big = NULL;

    // Align again during allocation. There is no need to align here
    // fix bug: not converted to unsigned char *, resulting in error in offset calculation (24x56)
    pool->first_small->last = (unsigned char *)pool + sizeof(mempool) + sizeof(small_trunk);
    pool->first_small->end = pool->first_small->last + size;
    pool->first_small->next = NULL;
    pool->first_small->failed = 0;

#if 0
    printf("pool:%p\n", pool);
    printf("first_small:%p\n", pool->first_small);
    printf("last:%p\n", pool->first_small->last);
    printf("end:%p\n", pool->first_small->end);
    printf("next:%p\n", pool->first_small->next);
    printf("failed:%p\n", &pool->first_small->failed);

    printf("first_small:%p, &first_small:%p, end:%p, end-first_small:%ld\n",
     pool->first_small, &pool->first_small, pool->first_small->end, 
     pool->first_small->end - (unsigned char*)pool->first_small);
#endif

#ifdef ENABLE_LOG
    printf("alloc pool addr:%p, border:%ld\n", pool, pool->border);
#endif
    return pool;
}

// Only large memory is released, and the last pointer of small memory is reset
void mempool_reset(mempool* pool) {
    big_trunk* cur_big = pool->big;
    while (cur_big) {
        if (cur_big->start) {
#ifdef ENABLE_LOG
            printf("free big trunk addr:%p\n", cur_big->start);
#endif
            free(cur_big->start);
            cur_big->start = NULL;
        }
        cur_big = cur_big->next;
    }
    pool->big = NULL;

    small_trunk* cur_small = pool->first_small;
    while (cur_small) {
        cur_small->last = (unsigned char*)cur_small + sizeof(small_trunk);
        cur_small->failed = 0;
        cur_small = cur_small->next;
    }
}

// The large structure is located on the small block. Release the large block first and then the small block
void mempool_destroy(mempool* pool) {
    big_trunk* cur_big = pool->big;
    while (cur_big) {
#ifdef ENABLE_LOG
        printf("free big trunk addr:%p\n", cur_big->start);
#endif
        free(cur_big->start);
        cur_big->start = NULL;
        cur_big = cur_big->next;
    }

    small_trunk* cur_small = pool->first_small->next;
    small_trunk* next_small;
    while (cur_small) {
        next_small = cur_small->next;
#ifdef ENABLE_LOG
        printf("free small trunk addr:%p\n", cur_small);
#endif
        free(cur_small);
        cur_small = next_small;
    }

#ifdef ENABLE_LOG
printf("free pool addr:%p\n", pool);
#endif

    free(pool);
}

/*
 * Allocate small structures and memory
 * Insert the new block into the tail of the block linked list (tail insertion method)
 * If the current block fails more than 4 times, point the current block pointer of the memory pool to the next block
 * Returns the value trunk after the unused memory start address alignment_ last
*/
static void* mempool_alloc_small(mempool* pool, size_t size) {
    small_trunk* new_trunk;
    size_t trunk_size = pool->first_small->end - (unsigned char*)pool->first_small;
    int ret = posix_memalign((void**)&new_trunk, MP_ALIGNMENT, trunk_size);
    if (0 != ret) {
        return NULL;
    }
#ifdef ENABLE_LOG
    printf("alloc small trunk addr:%p\n", new_trunk);
#endif
    memset(new_trunk, 0, trunk_size);

    unsigned char* trunk_start = (unsigned char*)new_trunk + sizeof(small_trunk);
    unsigned char* trunk_last = ALIGN_PTR(trunk_start, MP_ALIGNMENT);
    new_trunk->last = trunk_last + size;
    new_trunk->end = (unsigned char*)new_trunk + trunk_size;
    new_trunk->next = NULL;
    new_trunk->failed = 0;

    small_trunk* it;
    small_trunk* cur_small = pool->cur_small;
    for (it = pool->first_small; it->next; it = it->next) {
        if (++it->failed > 4) {
            cur_small = it->next;
        }
    }

    it->next = new_trunk;
    pool->cur_small = cur_small ? cur_small : new_trunk;

    return trunk_last;
}

/*
 * Allocate large blocks of memory
 * Judge whether the large linked list has unused nodes. If yes, bind the large memory to the node
 * If not, allocate a large node, bind a large memory, and insert the large node into the header of the large linked list (header insertion method)
 * Return the first address ret of large memory
*/
static void* mempool_alloc_big(mempool* pool, size_t size) {
    void* ret = malloc(size);
    if (!ret) {
        NULL;
    }
    memset(ret, 0, size);

    int n = 0;
    big_trunk* cur;
    for (cur = pool->big; cur && ++n > 3; cur = cur->next) {
        if (!cur->start) {
            cur->start = ret;
            return ret;
        }
    }
    big_trunk* new_node = mempool_alloc_trunk(pool, sizeof(big_trunk));
    if (!new_node) {
        free(ret);
        return NULL;
    }
    new_node->start = ret;
    new_node->next = pool->big;
    pool->big = new_node;

#ifdef ENABLE_LOG
    printf("get big trunk %p\n", ret);
#endif
    return ret;
}

/*
 * size Allocate large blocks if greater than the boundary value, otherwise allocate small blocks
 * When allocating small blocks, traverse the linked list of small blocks from the current small block, and return directly when finding unused space larger than size,
 * If not found, create a small block and return
*/
void* mempool_alloc_trunk(mempool* pool, size_t size) {
    if (size > pool->border) {
        return mempool_alloc_big(pool, size);
    }

    small_trunk* cur;
    for (cur = pool->cur_small; cur; cur = cur->next) {
        cur->last = ALIGN_PTR(cur->last, MP_ALIGNMENT);
        printf("end-last:%ld, last:%p, end:%p\n", cur->end - cur->last, cur->last, cur->end);
        if (cur->end - cur->last >= size) {
            void *ret = cur->last;
            cur->last = cur->last + size;
#ifdef ENABLE_LOG
            printf("get smaller trunk %p size %ld from small trunk %p\n", ret, size, cur);
#endif
            return ret;
        }
    }
    return mempool_alloc_small(pool, size);
}

// Release large blocks. Small blocks cannot be released with this function
void mempool_free_trunk(mempool* pool, void* trunk) {
    big_trunk* big = pool->big;
    while (big) {
        if (big->start == trunk) {
#ifdef ENABLE_LOG
            printf("free big trunk addr:%p\n", big->start);
#endif
            free(big->start);
            big->start = NULL;
            return;
        }
        big = big->next;
    }
}

int main() {
    mempool* pool = mempool_create(5000);
    int i;
    for (i = 0; i < 10; ++i) {
        void* trunk = mempool_alloc_trunk(pool, 1024);
        if (!trunk) {
            printf("alloc small failed\n");
            return -1;
        }
        // printf("alloc small success\n");
    }

    printf("\nalloc 10 big trunk\n");

    void* arr[10];
    for (i = 0; i < 10; ++i) {
        void* trunk = mempool_alloc_trunk(pool, 8000);
        if (!trunk) {
            printf("alloc big failed\n");
            return -1;
        }
        arr[i] = trunk;
    }

    printf("\nfree 10 big trunk\n");
    for (i = 0; i < 10; ++i) {
        mempool_free_trunk(pool, arr[i]);
    }

    printf("\nreset pool\n");
    mempool_reset(pool);

    for (i = 0; i < 10; ++i) {
        void* trunk = mempool_alloc_trunk(pool, 1024);
        if (!trunk) {
            printf("2 alloc small failed\n");
            return -1;
        }
    }

    printf("\ndestory pool\n");
    mempool_destroy(pool);
    pool = NULL;

    return 0;
}

Added by TimTimTimma on Fri, 17 Dec 2021 19:36:47 +0200