【C/C++】经典内存池的实现

1. 概述

本次经典内存池的结构较为简单,只支持单线程,在多线程情景下会发生很多意想不到的错误。不过后续作者会对其进行升级。
本次线程池能申请到的最大内存数量是256KB,参考了Google的tcmalloc项目。下面我们来讲解下本次内存池的实现方法。
你是一个计算机专业的大学生,自己做了一台小小的服务器并将其放在你电脑上运行。刚开始的时候没啥问题,但跑了十天半个月后你发现你的服务器发生了一些莫名其妙的问题(包括但不限于卡顿,闪退,无缘无故重启,内存分明够用但却申请不出来),这种情况还请不要怀疑,就是内存出问题了。
在你的服务器运行的时候,程序会频繁在堆内的申请/释放内存,但有的内存空间释放的快,有的内存释放的慢甚至不怎么销毁,这时侯就会出现内存碎片的问题:具体情况如下图所示:

memory_fragmentation

其中block2和block4没有被释放,它们之间有着一块块小的,分散的空间,这种碎掉的,分散的分布在内存里的那几块内存空间就叫做内存碎片,它会导致你后面想申请一块连续的完整的空间都申请不出来,同时也影响着程序的运行。内存碎片分为内碎片外碎片

类型 定义 比喻 后果
内碎片
(Internal fragmentation)
已分配的内存块里 多余未用的部分 快递箱里只放一支笔,却用鞋盒打包 → 盒内空位浪费 浪费内存、缓存污染
外碎片
(External fragmentation)
空闲的内存总量足够,但 没有连续的大块可用 书架上零散小空位,放不进一本厚书 → 空位用不上 malloc 失败、性能下降、甚至 OOM

内存池就是为了减少因为内存碎片带来的问题,它会提前申请一块较大的内存块,再将申请的内存块放入这个大空间内,最后再集中销毁。它有以下几个优势:

  1. 减少内存碎片:内存池是先向操作系统申请一整块大的内存空间,当人们申请内存空间(mallocnew)的时候,并不是直接向操作系统申请,而是向这一大块内存池申请。同理,当人们释放内存(deletefree)的时候,也不是直接归还给操作系统,而是归还给这个内存池。最后在程序退出/结束后集中将整个块销毁掉。这样就可以保证销毁后有一大块内存,减少内存碎片。
  2. 性能强:原本的mallocfree使用一次就得申请/释放一次内存空间,每一次申请就要找系统申请一块连续的内存空间,每一次释放就要销毁掉这块内存的内容,这样频繁的找系统申请和销毁是比较消耗性能的。而内存池不用频繁的找系统申请和销毁,大大加强了程序的性能。



2. 定长内存池

定长内存池指的是只分配同一种大小内存块的对象池,所有块长度在 创建时就固定,运行时不会改变。程序会先向系统申请一块大的内存,大内存块内有着一个个空闲块组成的隐式链表,每次申请的时候内存池都会分配一块固定长度的内存块。具体形式如下:

memory_page

2.1.内存池的属性和初始化

程序会向系统申请一个较大的内存空间,这种较大的内存空间就叫做页(page),定长内存池内有着许多的块(block),围绕这些,我们可以得出以下的属性:

memory_page.h

//定长内存池的页属性
typedef struct memory_page {
	//内存块属性
    size_t _block_size;				//每个内存块大小
    size_t _freed_block_num;  		//空闲的内存块数量
    
    //内存页属性
    size_t _page_capacity;			//内存页大小
    size_t _max_capacity;			//内存页最大容量
    void* _page;					//内存页的头指针
    void* _memory_pointer;			//内存指针,指向内存块用
} page_t;

使用它之前还得先初始化一下这个结构体内的属性,初始化内容如下:

  1. 内存块大小,空闲内存块的数量
  2. 内存块的容量,限制和隐式链表

memory_page.h

//初始化内存池
int init_memory_page(page_t* page, size_t block_size, size_t max_capacity) {
	//如果page错误或块大小为0:返回-1
    if (!page || block_size == 0) {
        return -1;
    }

    // 对齐,确保至少能放下一个指针
    if(block_size < 256*1024){
        block_size = select_align_number(block_size);
    }

    page->_block_size       = block_size;			//初始化块大小
    page->_freed_block_num  = INIT_PAGE_SIZE;		//初始化空闲块:这里设置为INIT_PAGE_SIZE = 5
    page->_page_capacity    = INIT_PAGE_SIZE;
    page->_max_capacity     = max_capacity;			//空闲块容量限制,0表示没限制

	//给内存页申请一大块内存(够放五个内存块)
    page->_page = malloc(INIT_PAGE_SIZE * block_size);	
    if (!page->_page){
    	return -1;
    }

	//初始化隐式链表
    page->_memory_pointer = page->_page;				//先指向page头
    char* cur = (char*)page->_memory_pointer;
    for (size_t i = 0; i < INIT_PAGE_SIZE - 1; ++i) {
        *(char**)cur = cur + block_size;				//定义当前指向元素的下一个元素
        cur += block_size;								//当前指针后移
    }
    *(char**)cur = NULL;								//最末尾指向空
    return 0;
}

2.2. 定长内存池的申请

定长内存池的申请说实话就是对隐式链表的头删,插入操作过程如下:

  1. 将内存指针_memory_pointer指向的块拿出来。
  2. 然后将内存指针指向被拿出的内存块的下一个元素。
  3. 空闲块-1。
    具体操作如下图所示:

push_page

代码表述如下:

memory_page.h

void* push_block(page_t* page) {
    if (!page || !page->_page){
        return NULL;
    }
	
	//如果用池内的元素用完了
    if (page->_freed_block_num == 0) {
    	//扩容
        if (resize_page(page) != 0){
            return NULL;
        }
    }

    void* block = page->_memory_pointer;
    if (block) {
        page->_memory_pointer = *(char**)block;	//内存指针后移
        page->_freed_block_num--;				//空闲块数量减一
    }
    return block;
}

值得注意的是,push操作内有一个扩容操作resize_page,当空闲块为0的时候会触发resize_page,触发后内存页会扩容一半,具体操作如下:

  1. 扩大旧内存。
  2. 创建新增块,将其连接成隐式链表。
  3. 将新增块组成的链表连到老链表后头。

代码表述如下:

memory_page.h

//扩容内存页
static int resize_page(page_t* page) {
    if (!page || !page->_page){
        return -1;
    }

    if (page->_max_capacity && page->_page_capacity >= page->_max_capacity){
        return -1;
    } 

    size_t new_cap = page->_page_capacity + (page->_page_capacity / 2);
    if (page->_max_capacity && new_cap > page->_max_capacity){
         new_cap = page->_max_capacity;
    }

	//扩容旧的内存块
    char* new_page = (char*)realloc(page->_page, new_cap * page->_block_size);
    if (!new_page){
        return -1;
    }

    // 仅增量链接,避免整表重建
    char* old_tail = new_page + (page->_page_capacity * page->_block_size);
    char* cur = old_tail;
    //创建新增的块,并加入连成隐式链表
    for (size_t i = page->_page_capacity; i < new_cap - 1; ++i) {
        *(char**)cur = cur + page->_block_size;
        cur += page->_block_size;
    }
    *(char**)cur = (char*)page->_memory_pointer;   // 挂到原链表头
    page->_memory_pointer = old_tail;	//将内存指针放在旧链表尾部

	//初始化新增块
    page->_page           = new_page;
    page->_freed_block_num += new_cap - page->_page_capacity;
    page->_page_capacity  = new_cap;
    return 0;
}

2.3. 定长内存池的归还

定长内存池的归还其实就是隐式链表的头插,它的操作过程如下所示:

  1. 将内存指针_memory_pointer指针前移。
  2. 然后将内存指针指向的块连接回空闲链表。
  3. 空闲块+1。

用图表示其过程就是下面这种样子:

pop_page

这种过程用代码表述如下图所示:

memory_page.h

void pop_block(page_t* page, void* block) {
    if (!page || !page->_page || !block){
        return;
    }

    // 边界检查
    uintptr_t base = (uintptr_t)page->_page;
    uintptr_t end  = base + page->_page_capacity * page->_block_size;
    uintptr_t ptr  = (uintptr_t)block;
    if (ptr < base || ptr >= end || ((ptr - base) % page->_block_size) != 0){
        return;
    }

    *(char**)block = (char*)page->_memory_pointer;	//内存指针前移动
    page->_memory_pointer = block;
    page->_freed_block_num++;						//空闲内存块+1
}

2.4. 销毁

明白了上面那两章的内容,你或许会发现一点:就算是pop了,但使用过的block却还是没有销毁,只是将其归还给了空闲链表。这其实就是池化思想的精髓:先不释放,等到程序结束后再释放,以减少频繁让系统销毁造成的开销。这里也是一样的,等到程序退出了才将整个页连同里面的内存块一块销毁,减少频繁的向系统申请/让系统销毁。
销毁的代码如下:

memory_page.h

void destroy_memory_page(page_t* page) {
    if (page) {
        free(page->_page);				  //调用系统的free释放整个内存页
        memset(page, 0, sizeof(*page));   // 清零结构体
    }
}

销毁的代码往往是在析构方法中使用,后面会有的。



3. 管理不同内存块大小的页

前面的内存池的块只是固定的大小,要是申请一个大于该内存页内块的大小的内存块那大底就是要出错的(一般是段错误,因为如果内存块们的大小超过了内存页大小,就可能会发生段错误)。
那要是需要用定长内存池,又需要这个内存池能管理不同大小的内存块呢?将内存页全部变成每块256KB的行不行?当然可以,但会造成巨大的内存浪费,比方说本来一个内存块只需要1KB的空间就可以装下,内存池非要给256KB,内存浪费99.2%,这不是一个很明智的选择。好在google的tcmalloc给出了一套方案:使用数组的槽来装载管理不同大小内存块的内存页,比方说arr[1]的页每块只分配8B,arr[2]每块分配16KB...具体如下所示:

slot_page

看起来很像哈希表是吗?这种也确实是借鉴了哈希表的思想。但槽的数量该怎么控制呢?如果是从1KB到256KB每个都设槽,那岂不是要256*1024=262144个?那样可太耗费空间了!但好在可以使用分治的方法:对 一个范围内的内存块 进行向上对齐,例如申请98字节,直接给你128字节,那此时问题又来了,这个范围取多大合适呢?取大了取小了均不合适,所以采取了分治原则,对于不同的大小范围采取了不同的向上取整原则(理由如下:对于开辟空间小的时候,对齐数选择较小;对于开辟空间大的时候,对齐数选择较大,这样才能使有效空间的利用率变大,减少内碎片问题。当然再小也不能小于一个指针的大小,因为自由链表自身需要进行链接),具体如下:

字节范围 (含左不含右) 对齐数 (字节) 哈希桶下标范围
[1, 128] 8 [0, 16)
[129, 1024] 16 [16, 72)
[1025, 8*1024] 128 [72, 128)
[8*1024+1, 64*1024] 1024 [128, 184)
[64*1024+1, 256*1024] 8*1024 [184, 208)

我们可以试着计算下它的平均内存浪费率:

  1. 对于129来说,它使用的对齐数就是16,所以它离最近的一个对齐数128+16=144相差15,此时的浪费率就是 15/144 约为10% 当申请的字节数在该区间内继续往上增加时,浪费个数只会小于等于15,即分子小于等于15,而总空间数越来越大,即分母越来越大,即浪费率只会小于10%
  2. 对于1024来说,它使用的对齐数就是128,所以它离最近的一个对齐数1024+128=1152相差127,此时的浪费率就是127/1152 约为11%
  3. 推导过程同上......所以该设计方案的浪费率控制在了10%左右

因此你会发现这个数组的每个槽的页内一个块的大小如下:

  1. 8B对齐:8,16,24,32 ... 128(16个槽)
  2. 16B对齐:128 144 160 176 ... 1024 (56个槽)
    ...

3.1. 内存池线性表

3.1.1. 内存池线性表的属性和初始化

既然想要使用线性表管理,那我们就写一个线性表,一个线性表每一个格就是一个槽,每一个槽里面有一个内存页,围绕这些,我们可以得出以下结构体:

memory_array_list.h

//数组结构体
typedef struct memory_array_list{
    page_t* _page_slot;					//数组槽(装内存页用的)
    size_t _pool_size;					//数组大小
}list_t;

当然在使用之前也是要初始化的,由于该内存池的槽总共是240,所以初始化直接插入240个元素。每个槽里面接空指针,等要插入内存块的时候再申请page进槽里。

memory_array_list.h

int init_memory_array_list(list_t* memory_array_list) {
    if (!memory_array_list) {
        return -1;
    }

    // 分配并清零 240 个 page_t
    memory_array_list->_page_slot = (page_t*)calloc(MAX_CAPACITY_NUMBER, sizeof(page_t));
    if (!memory_array_list->_page_slot) {
        return -1;  // 分配失败
    }

    memory_array_list->_pool_size = MAX_CAPACITY_NUMBER;
    return 0;
}

3.1.2.内存池线性表的插入和删除

经常被数据结构爱的人都知道,线性表的插入操作就是从中找到一个格,先将其后的块向后移动再插入进去。当然这确实有点耗费性能,但它的尾部插入不需要移动任何元素,性能比较好,本次内存池主要用的也是这个。
可以用一个图来复习下这个内容:

array_insert

使用代码表述如下:

memory_array_list.h

//插入操作
void push(list_t* memory_array_list, page_t value, size_t position) {
    if (!memory_array_list || memory_array_list->_pool_size >= MAX_CAPACITY_NUMBER || position > memory_array_list->_pool_size) {
        printf("Push failed: invalid state\n");
        return;
    }
	//移动插入位置后面的元素
    for (size_t i = memory_array_list->_pool_size; i > position; --i) {
        memory_array_list->_page_slot[i] = memory_array_list->_page_slot[i - 1];
    }
    //插入元素
    memory_array_list->_page_slot[position] = value;
    //内存池的大小+1
    memory_array_list->_pool_size++;
}

//头插法
void push_front(list_t* memory_array_list, page_t push_value){
    push(memory_array_list, push_value, 0);
}

//尾插法
void push_back(list_t* memory_array_list, page_t push_value){
    push(memory_array_list, push_value, memory_array_list->_pool_size);
}

删除操作也是如此,就是移除元素,然后后续元素前移动:


array_erase

代码表述如下:

memory_array_list.h

//删除
void erase(list_t* memory_array_list, size_t position) {
    if (!memory_array_list || empty(memory_array_list) || position >= memory_array_list->_pool_size) {
        printf("Erase failed: invalid state\n");
        return;
    }

    for (size_t i = position; i < memory_array_list->_pool_size - 1; ++i) {
        memory_array_list->_page_slot[i] = memory_array_list->_page_slot[i + 1];
    }
    memory_array_list->_pool_size--;
}

//头部删除
void erase_front(list_t* memory_array_list){
    erase(memory_array_list, 0);
}

//尾部删除
void erase_back(list_t* memory_array_list){
    erase(memory_array_list, memory_array_list->_pool_size - 1);
}

3.1.3. 线性表内元素销毁

前面提到单页的内存池的pop操作仅仅是归还给该页的隐式空闲链表,这里也是如此,槽内的每一页的pop操作也只是归还给该槽内内存页的隐式空闲链表内,要彻底销毁掉就得使用一些方法销毁掉。这里提供了一个叫destroy_memory_array_list的方法,可以将线性表内的所有元素清掉。代码表述如下:

memory_array_list.h

//清除线性表内元素
static void destroy_memory_array_list(list_t* memory_pool) {
    if (memory_pool) {
    	//遍历,清除每个槽的page
        for (size_t i = 0; i < memory_pool->_pool_size; i++) {
            destroy_memory_page(&memory_pool->_page_slot[i]);
        }
        //调用系统的free释放掉page
        free(memory_pool->_page_slot);
        memory_pool->_page_slot = NULL; //槽置空
        memory_pool->_pool_size = 0;
    }
}

3.2. 寻找对齐数与合适下标

前面提到了对齐数的寻找规则,不同区间的内存块分配不同大小的对齐数,比方说127就分配128大的内存块(8的倍数),140就分配144大的内存块(16的倍数),寻找对齐数的方法如下:

common_instrument.h

#include <stdio.h>

//寻找对齐数
inline size_t align_up(size_t size, size_t align) {
    if (size % align == 0) {
        return size;
    } else {
        return size + align - (size % align);
    }
}

//按照内存块大小选择对齐数
inline size_t select_align_number(size_t size){
    if(size > 1 && size <= 128){
        return align_up(size, 8);
    }
    else if(size > 128 && size <= 1024){
        return align_up(size, 16);
    }
    else if(size > 1024 && size <= 8*1024){
        return align_up(size, 128);
    }
    else if(size > 8*1024 && size <= 64*1024){
        return align_up(size, 1024);
    }
    else if(size > 64*1024 && size <= 256*1024){
        return align_up(size, 8*1024);
    }
    else{
        return -1;
    }
}

寻找完对齐数,我们就可以按照对齐数将内存块插入到合适的槽内了,对齐数和下标关系如下公式所示:

区间范围(字节) 对齐 align 下标公式(C 代码)
[1, 128] 8 index = (aligned / 8) - 1
[129, 1024] 16 index = 16 + ((aligned - 128) / 16)
[1025, 8×1024] 128 index = 72 + ((aligned - 1024) / 128)
[8×1024+1, 64×1024] 1024 index = 128 + ((aligned - 8×1024) / 1024)
[64×1024+1, 256×1024] 8×1024 index = 184 + ((aligned - 64×1024) / (8×1024))

代码表示如下:

memory_pool.h

在这里插入代码片#define SLOT_NUM_1 16
#define SLOT_NUM_2 56

static list_t memory_pool;  // 全局内存池

//返回下标
static size_t return_index(size_t block_size, int align_number) {
    if (block_size % align_number == 0) {
        return block_size / align_number - 1;
    } 
    else {
        return block_size / align_number;
    }
}

//寻找合适的下标
static size_t find_index(size_t block_size) {
    if (block_size > 0 && block_size <= 128) {
        return return_index(block_size, 8);
    } 
    else if (block_size > 128 && block_size <= 1024) {
        return return_index(block_size - 128, 16) + SLOT_NUM_1;
    } 
    else if (block_size > 1024 && block_size <= 1024 * 8) {
        return return_index(block_size - 1024, 128) + SLOT_NUM_1 + SLOT_NUM_2;
    } 
    else if (block_size >= 1024 * 8 && block_size < 1024 * 64) {
        return return_index(block_size - 8 * 1024, 1024) + SLOT_NUM_1 + 2 * SLOT_NUM_2;
    } 
    else if (block_size >= 1024 * 64 && block_size < 1024 * 256) {
        return return_index(block_size - 1024 * 64, 1024 * 8) + SLOT_NUM_1 + 3 * SLOT_NUM_2;
    } 
    else {
        return (size_t)-1;
    }
}

3.3. 插入和释放内存块

在为内存块找到合适的对齐数和下标后就可以开始插入了,这里使用的是_malloc方法,当时候可以使用宏定义伪装一下:

memory_pool.h

//申请
void* _malloc(size_t size) {
    if (size == 0 || size > 256 * 1024) {
        return NULL;  // 不支持 0 或超过 256KB 的请求
    }

    // 1. 计算对齐后的 block_size 和槽位 index
    size_t block_size = select_align_number(size);  // 对齐
    size_t index = find_index(block_size);         // 槽位
    if (index == (size_t)-1) {
        return NULL;  // 无效大小
    }

    // 2. 从对应的 page_t 分配内存
    page_t* page = &memory_pool._page_slot[index];
    void* block = push_block(page);
    if (!block) {
        // 分配失败(如槽位已满),可以尝试扩容
        if (resize_page(page) != 0) {
            return NULL;  // 扩容失败
        }
        block = push_block(page);  // 再次尝试插入
    }

    return block;
}

释放内存也是如此,找好目标块,按照它所在页进行pop操作

memory_pool.h

void _free(void* free_block) {
    if (!free_block) {
        return;
    }

    // 1. 遍历所有 page_t,检查 free_block 是否属于该页
    for (size_t i = 0; i < memory_pool._pool_size; i++) {
        page_t* page = &memory_pool._page_slot[i];
        uintptr_t base = (uintptr_t)page->_page;
        uintptr_t end = base + page->_page_capacity * page->_block_size;
        uintptr_t ptr = (uintptr_t)free_block;

        // 检查是否在当前页范围内,并且地址对齐
        if (ptr >= base && ptr < end && ((ptr - base) % page->_block_size == 0)) {
            pop_block(page, free_block);  // 释放内存
            return;
        }
    }

最后使用宏定义伪装一下 \ (^ o ^ )/

memory_pool.h

#define malloc(size) _malloc(size)
#define free(free_block) _free(free_block)

3.4. 构造和析构

C语言内貌似没有析构和构造,不过可以使用编译器扩展或链接器技巧实现,构造方法使用init_memory_pool的逻辑,init_memory_pool逻辑如下:

memory_pool.h

static void init_memory_pool() {
    init_memory_array_list(&memory_pool);
    for (size_t i = 0; i < memory_pool._pool_size; i++) {
        size_t block_size;
        
        // 1. 1~128 字节(8 字节对齐)
        if (i < SLOT_NUM_1) {
            block_size = (i + 1) * 8;  // 8, 16, 24, ..., 128
        } 
        // 2. 129~1024 字节(16 字节对齐)
        else if (i < SLOT_NUM_1 + SLOT_NUM_2) {
            block_size = 128 + (i - SLOT_NUM_1 + 1) * 16;  // 144, 160, ..., 1024
        } 
        // 3. 1025~8KB(128 字节对齐)
        else if (i < SLOT_NUM_1 + 2 * SLOT_NUM_2) {
            block_size = 1024 + (i - SLOT_NUM_1 - SLOT_NUM_2 + 1) * 128;  // 1152, 1280, ..., 8KB
        }
        // 4. 8KB~64KB(1KB 对齐)
        else if (i < SLOT_NUM_1 + 3 * SLOT_NUM_2) {
            block_size = 8 * 1024 + (i - SLOT_NUM_1 - 2 * SLOT_NUM_2) * 1024;  // 9KB, 10KB, ..., 64KB
        }
        // 5. 64KB~256KB(8KB 对齐)
        else {
            block_size = 64 * 1024 + (i - SLOT_NUM_1 - 3 * SLOT_NUM_2) * 8 * 1024;  // 72KB, 80KB, ..., 256KB
        }
        
        init_memory_page(&memory_pool._page_slot[i], block_size, 0);
    }
}

析构则是使用destroy_memory_array_list,在程序退出后立马使用。
构造方法和析构方法的代码如下所示:

memory_pool.h

#if defined(__GNUC__) || defined(__clang__)
    // GNU/clang 编译器:声明和定义分开
    #define DECLARE_CONSTRUCTOR(fn) static void fn(void) __attribute__((constructor));
    #define DECLARE_DESTRUCTOR(fn)  static void fn(void) __attribute__((destructor));
    #define DEFINE_FUNCTION(fn) static void fn(void)
#elif defined(_MSC_VER)
    // MSVC 编译器
    #define DECLARE_CONSTRUCTOR(fn) static void fn(void);
    #define DECLARE_DESTRUCTOR(fn)  static void fn(void);
    #define DEFINE_FUNCTION(fn) static void fn(void)
#else
    // 其他编译器
    #error "Unsupported compiler"
#endif

static bool pool_inited = false;

// 声明构造函数/析构函数
DECLARE_CONSTRUCTOR(auto_init)
DECLARE_DESTRUCTOR(auto_fini)

// 定义函数(不带属性)
DEFINE_FUNCTION(auto_init) {
    if (!pool_inited) { 
        init_memory_pool(); 
        pool_inited = true; 
    }
}

DEFINE_FUNCTION(auto_fini) {
    destroy_memory_array_list(&memory_pool); 
    pool_inited = false;
}

这样一个经典的内存池就制作完毕了,但它的线程安全基本为0,因此下一章我将写出它的进阶版本:高并发内存池(参考google 的tcmalloc)



4. 源代码

common_instrument.h

#include <stdio.h>

// ---------- 工具 ----------
inline size_t align_up(size_t size, size_t align) {
    if (size % align == 0) {
        return size;
    } else {
        return size + align - (size % align);
    }
}

inline size_t select_align_number(size_t size){
    if(size > 1 && size <= 128){
        return align_up(size, 8);
    }
    else if(size > 128 && size <= 1024){
        return align_up(size, 16);
    }
    else if(size > 1024 && size <= 8*1024){
        return align_up(size, 128);
    }
    else if(size > 8*1024 && size <= 64*1024){
        return align_up(size, 1024);
    }
    else if(size > 64*1024 && size <= 256*1024){
        return align_up(size, 8*1024);
    }
    else{
        return -1;
    }
}

memory_page.h

#ifndef MEMORY_PAGE_H
#define MEMORY_PAGE_H

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>   
#include "common_instrument.h"

#define INIT_PAGE_SIZE 8

//定长内存池的页属性
typedef struct memory_page {
	//内存块属性
    size_t _block_size;				//每个内存块大小
    size_t _freed_block_num;  		//空闲的内存块数量
    
    //内存页属性
    size_t _page_capacity;			//内存页大小
    size_t _max_capacity;			//内存页最大容量
    void* _page;					//内存页的头指针
    void* _memory_pointer;			//内存指针,指向内存块用
} page_t;

//初始化内存池
int init_memory_page(page_t* page, size_t block_size, size_t max_capacity) {
	//如果page错误或块大小为0:返回-1
    if (!page || block_size == 0) {
        return -1;
    }

    // 对齐,确保至少能放下一个指针
    if(block_size < 256*1024){
        block_size = select_align_number(block_size);
    }

    page->_block_size       = block_size;			//初始化块大小
    page->_freed_block_num  = INIT_PAGE_SIZE;		//初始化空闲块:这里设置为INIT_PAGE_SIZE = 5
    page->_page_capacity    = INIT_PAGE_SIZE;
    page->_max_capacity     = max_capacity;			//空闲块容量限制,0表示没限制

	//给内存页申请一大块内存(够放五个内存块)
    page->_page = malloc(INIT_PAGE_SIZE * block_size);	
    if (!page->_page){
    	return -1;
    }

	//初始化隐式链表
    page->_memory_pointer = page->_page;				//先指向page头
    char* cur = (char*)page->_memory_pointer;
    for (size_t i = 0; i < INIT_PAGE_SIZE - 1; ++i) {
        *(char**)cur = cur + block_size;				//定义当前指向元素的下一个元素
        cur += block_size;								//当前指针后移
    }
    *(char**)cur = NULL;								//最末尾指向空
    return 0;
}

//扩容内存页
static int resize_page(page_t* page) {
    if (!page || !page->_page){
        return -1;
    }

    if (page->_max_capacity && page->_page_capacity >= page->_max_capacity){
        return -1;
    } 

    size_t new_cap = page->_page_capacity + (page->_page_capacity / 2);
    if (page->_max_capacity && new_cap > page->_max_capacity){
         new_cap = page->_max_capacity;
    }

	//扩容旧的内存块
    char* new_page = (char*)realloc(page->_page, new_cap * page->_block_size);
    if (!new_page){
        return -1;
    }

    // 仅增量链接,避免整表重建
    char* old_tail = new_page + (page->_page_capacity * page->_block_size);
    char* cur = old_tail;
    //创建新增的块,并加入连成隐式链表
    for (size_t i = page->_page_capacity; i < new_cap - 1; ++i) {
        *(char**)cur = cur + page->_block_size;
        cur += page->_block_size;
    }
    *(char**)cur = (char*)page->_memory_pointer;   // 挂到原链表头
    page->_memory_pointer = old_tail;	//将内存指针放在旧链表尾部

	//初始化新增块
    page->_page           = new_page;
    page->_freed_block_num += new_cap - page->_page_capacity;
    page->_page_capacity  = new_cap;
    return 0;
}

// 分配
void* push_block(page_t* page) {
    if (!page || !page->_page){
        return NULL;
    }

    if (page->_freed_block_num == 0) {
        if (resize_page(page) != 0){
            return NULL;
        }
    }

    void* block = page->_memory_pointer;
    if (block) {
        page->_memory_pointer = *(char**)block;
        page->_freed_block_num--;
    }
    return block;
}

// 归还 
void pop_block(page_t* page, void* block) {
    if (!page || !page->_page || !block){
        return;
    }

    // 边界检查
    uintptr_t base = (uintptr_t)page->_page;
    uintptr_t end  = base + page->_page_capacity * page->_block_size;
    uintptr_t ptr  = (uintptr_t)block;
    if (ptr < base || ptr >= end || ((ptr - base) % page->_block_size) != 0){
        return;
    }

    *(char**)block = (char*)page->_memory_pointer;	//内存指针前移动
    page->_memory_pointer = block;
    page->_freed_block_num++;						//空闲内存块+1
}

// 销毁 
void destroy_memory_page(page_t* page) {
    if (page) {
        free(page->_page);				  //调用系统的free释放整个内存页
        memset(page, 0, sizeof(*page));   // 清零结构体
    }
}

#endif

memory_array_list.h

#ifndef MEMORY_ARRAY_LIST_H
#define MEMORY_ARRAY_LIST_H

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "memory_page.h"

#define MAX_CAPACITY_NUMBER 240

typedef struct memory_array_list{
    page_t* _page_slot;
    size_t _pool_size;
}list_t;

static void destroy_memory_array_list(list_t* memory_pool) {
    if (memory_pool) {
        for (size_t i = 0; i < memory_pool->_pool_size; i++) {
            destroy_memory_page(&memory_pool->_page_slot[i]);
        }
        free(memory_pool->_page_slot);
        memory_pool->_page_slot = NULL;
        memory_pool->_pool_size = 0;
    }
}

int init_memory_array_list(list_t* memory_array_list) {
    if (!memory_array_list) {
        return -1;
    }

    // 分配并清零 240 个 page_t
    memory_array_list->_page_slot = (page_t*)calloc(MAX_CAPACITY_NUMBER, sizeof(page_t));
    if (!memory_array_list->_page_slot) {
        return -1;  // 分配失败
    }

    memory_array_list->_pool_size = MAX_CAPACITY_NUMBER;
    return 0;
}

bool empty(list_t* memory_array_list){
    return memory_array_list->_pool_size == 0;
}

void push(list_t* memory_array_list, page_t value, size_t position) {
    if (!memory_array_list || memory_array_list->_pool_size >= MAX_CAPACITY_NUMBER || position > memory_array_list->_pool_size) {
        printf("Push failed: invalid state\n");
        return;
    }

    for (size_t i = memory_array_list->_pool_size; i > position; --i) {
        memory_array_list->_page_slot[i] = memory_array_list->_page_slot[i - 1];
    }
    memory_array_list->_page_slot[position] = value;
    memory_array_list->_pool_size++;
}

void erase(list_t* memory_array_list, size_t position) {
    if (!memory_array_list || empty(memory_array_list) || position >= memory_array_list->_pool_size) {
        printf("Erase failed: invalid state\n");
        return;
    }

    for (size_t i = position; i < memory_array_list->_pool_size - 1; ++i) {
        memory_array_list->_page_slot[i] = memory_array_list->_page_slot[i + 1];
    }
    memory_array_list->_pool_size--;
}

void push_front(list_t* memory_array_list, page_t push_value){
    push(memory_array_list, push_value, 0);
}

void push_back(list_t* memory_array_list, page_t push_value){
    push(memory_array_list, push_value, memory_array_list->_pool_size);
}

void erase_front(list_t* memory_array_list){
    erase(memory_array_list, 0);
}

void erase_back(list_t* memory_array_list){
    erase(memory_array_list, memory_array_list->_pool_size - 1);
}

#endif

memory_pool.h

#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H

#include "memory_array_list.h"

#define SLOT_NUM_1 16
#define SLOT_NUM_2 56

static list_t memory_pool;  // 全局内存池

static size_t return_index(size_t block_size, int align_number) {
    if (block_size % align_number == 0) {
        return block_size / align_number - 1;
    } 
    else {
        return block_size / align_number;
    }
}

static size_t find_index(size_t block_size) {
    if (block_size > 0 && block_size <= 128) {
        return return_index(block_size, 8);
    } 
    else if (block_size > 128 && block_size <= 1024) {
        return return_index(block_size - 128, 16) + SLOT_NUM_1;
    } 
    else if (block_size > 1024 && block_size <= 1024 * 8) {
        return return_index(block_size - 1024, 128) + SLOT_NUM_1 + SLOT_NUM_2;
    } 
    else if (block_size >= 1024 * 8 && block_size < 1024 * 64) {
        return return_index(block_size - 8 * 1024, 1024) + SLOT_NUM_1 + 2 * SLOT_NUM_2;
    } 
    else if (block_size >= 1024 * 64 && block_size < 1024 * 256) {
        return return_index(block_size - 1024 * 64, 1024 * 8) + SLOT_NUM_1 + 3 * SLOT_NUM_2;
    } 
    else {
        return (size_t)-1;
    }
}

static void init_memory_pool() {
    init_memory_array_list(&memory_pool);
    for (size_t i = 0; i < memory_pool._pool_size; i++) {
        size_t block_size;
        
        // 1. 1~128 字节(8 字节对齐)
        if (i < SLOT_NUM_1) {
            block_size = (i + 1) * 8;  // 8, 16, 24, ..., 128
        } 
        // 2. 129~1024 字节(16 字节对齐)
        else if (i < SLOT_NUM_1 + SLOT_NUM_2) {
            block_size = 128 + (i - SLOT_NUM_1 + 1) * 16;  // 144, 160, ..., 1024
        } 
        // 3. 1025~8KB(128 字节对齐)
        else if (i < SLOT_NUM_1 + 2 * SLOT_NUM_2) {
            block_size = 1024 + (i - SLOT_NUM_1 - SLOT_NUM_2 + 1) * 128;  // 1152, 1280, ..., 8KB
        }
        // 4. 8KB~64KB(1KB 对齐)
        else if (i < SLOT_NUM_1 + 3 * SLOT_NUM_2) {
            block_size = 8 * 1024 + (i - SLOT_NUM_1 - 2 * SLOT_NUM_2) * 1024;  // 9KB, 10KB, ..., 64KB
        }
        // 5. 64KB~256KB(8KB 对齐)
        else {
            block_size = 64 * 1024 + (i - SLOT_NUM_1 - 3 * SLOT_NUM_2) * 8 * 1024;  // 72KB, 80KB, ..., 256KB
        }
        
        init_memory_page(&memory_pool._page_slot[i], block_size, 0);
    }
}

#if defined(__GNUC__) || defined(__clang__)
    // GNU/clang 编译器:声明和定义分开
    #define DECLARE_CONSTRUCTOR(fn) static void fn(void) __attribute__((constructor));
    #define DECLARE_DESTRUCTOR(fn)  static void fn(void) __attribute__((destructor));
    #define DEFINE_FUNCTION(fn) static void fn(void)
#elif defined(_MSC_VER)
    // MSVC 编译器
    #define DECLARE_CONSTRUCTOR(fn) static void fn(void);
    #define DECLARE_DESTRUCTOR(fn)  static void fn(void);
    #define DEFINE_FUNCTION(fn) static void fn(void)
#else
    // 其他编译器
    #error "Unsupported compiler"
#endif

static bool pool_inited = false;

// 声明构造函数/析构函数
DECLARE_CONSTRUCTOR(auto_init)
DECLARE_DESTRUCTOR(auto_fini)

// 定义函数(不带属性)
DEFINE_FUNCTION(auto_init) {
    if (!pool_inited) { 
        init_memory_pool(); 
        pool_inited = true; 
    }
}

DEFINE_FUNCTION(auto_fini) {
    destroy_memory_array_list(&memory_pool); 
    pool_inited = false;
}


void* _malloc(size_t size) {
    if (size == 0 || size > 256 * 1024) {
        return NULL;  // 不支持 0 或超过 256KB 的请求
    }

    // 1. 计算对齐后的 block_size 和槽位 index
    size_t block_size = select_align_number(size);  // 对齐
    size_t index = find_index(block_size);         // 槽位
    if (index == (size_t)-1) {
        return NULL;  // 无效大小
    }

    // 2. 从对应的 page_t 分配内存
    page_t* page = &memory_pool._page_slot[index];
    void* block = push_block(page);
    if (!block) {
        // 分配失败(如槽位已满),可以尝试扩容
        if (resize_page(page) != 0) {
            return NULL;  // 扩容失败
        }
        block = push_block(page);  // 再次尝试
    }

    return block;
}

void _free(void* free_block) {
    if (!free_block) {
        return;
    }

    // 1. 遍历所有 page_t,检查 free_block 是否属于该页
    for (size_t i = 0; i < memory_pool._pool_size; i++) {
        page_t* page = &memory_pool._page_slot[i];
        uintptr_t base = (uintptr_t)page->_page;
        uintptr_t end = base + page->_page_capacity * page->_block_size;
        uintptr_t ptr = (uintptr_t)free_block;

        // 检查是否在当前页范围内,并且地址对齐
        if (ptr >= base && ptr < end && ((ptr - base) % page->_block_size == 0)) {
            pop_block(page, free_block);  // 释放内存
            return;
        }
    }

    // 如果未找到所属页,可能是非法指针
    printf("Error: Invalid free_block %p\n", free_block);
}

#define malloc(size) _malloc(size)
#define free(free_block) _free(free_block)

#endif

posted on 2025-07-26 10:21  楊思瞻  阅读(28)  评论(0)    收藏  举报

导航