原来手撕内存池这么这么简单!

如果上一关于线程池的文章原来手撕线程池这么简单!你能读懂,那么这篇内存池的实现你一定不要错过,因为真的非常非常容易理解。

1. 内存池的概念和优势

定义:

内存池是一种内存管理策略,通过预先分配一大块内存,并将其切分成多个固定大小的小块,供程序在运行时使用。这种方法通常用于需要快速且频繁地分配和释放小量内存的应用程序。

优势:
  • 减少内存碎片: 传统的内存分配方式(如使用 mallocfree)可能导致内存碎片,而内存池通过固定大小的块分配减少了这种碎片。关于内存碎片,文末会更细致讨论。
  • 提高效率: 内存池允许快速分配和释放,因为它避免了每次分配时的内存搜索和合并操作,这些操作通常在使用通用内存分配函数时必需。
  • 预测性内存使用: 由于内存是预先分配的,应用程序的内存使用更可预测,有助于避免运行时内存不足的问题。

2. 设计内存池的挑战

块大小选择:

选择合适的块大小是设计内存池时的一个主要挑战。块大小需要根据应用程序的具体需求精心选择,以最小化内存浪费和管理开销。太小的块可能导致管理开销过大,而太大的块可能导致内存浪费。此外,应用程序可能需要不同大小的内存块来处理不同的数据结构,这需要设计多个内存池或一个能够动态调整块大小的池。

内存对齐:

内存对齐是确保内存分配满足硬件访问要求的过程。不正确的对齐可能会导致CPU访问内存时增加额外的开销,影响性能。在内存池设计中,需要确保每个内存块都按照一定的对齐规则(通常是几个字节或机器字的倍数)来分配,以优化内存访问速度和效率。

这两个挑战需要在设计内存池时仔细考量,以确保内存池既能提高性能又能满足应用需求。在你的博客中,你可以通过提供实际的代码示例和性能测试结果,来更具体地展示这些概念和设计选择是如何在实际应用中工作的。

内存碎片是在分配和释放内存时出现的一种现象,可以分为两种类型:外部碎片和内部碎片。在传统的内存管理策略中,这种碎片化经常发生,尤其是在内存被频繁分配和释放的场景下。内存池通过管理固定大小的内存块来显著减少这种碎片化,下面详细介绍这一过程。

3. 内存池实现

代码如果有不理解的地方,可以评论区留言。双指针部分是比较难理解的,可以在原来手撕线程池这么简单!这篇中有看到双指针的解释。

结构和初始化

在此代码中,有两个主要的结构体用于实现内存池管理:mempool_tmp_pool_t。当然这两个名字可以根据喜好自己取。

  • mempool_t:

    • block_size:定义内存块的大小。
    • free_count:记录空闲块的数量。
    • free_ptr:指向当前空闲块的指针。
    • mem:指向内存池中所有内存块的起始位置。
  • mp_pool_t:

    • pools:包含多个 mempool_t 结构,每个结构代表一个不同块大小的内存池。

初始化内存池 (mp_pool_init):

初始化过程涉及为每种块大小分配一整块内存,并在这块内存内部建立一个简单的链表来管理单独的内存块。每个块的开头存储了指向下一个空闲块的指针。这样,每个内存池可以快速访问和分配其内部的空闲块。

int mp_pool_init(mp_pool_t *mp) {
    if (!mp) return -1;
    for (int i = 0; i < NUM_SIZES; i++) {
        mempool_t *pool = &mp->pools[i];
        pool->block_size = block_sizes[i];
        pool->free_count = MEM_PAGE_SIZE / pool->block_size;
        pool->mem = (char *)malloc(MEM_PAGE_SIZE);
        if (!pool->mem) return -1;
        pool->free_ptr = pool->mem;

        char *ptr = pool->free_ptr;
        for (int j = 0; j < pool->free_count - 1; j++) {
            *(char **)ptr = ptr + pool->block_size;
            ptr += pool->block_size;
        }
        *(char **)ptr = NULL;
    }
    return 0;
}
内存分配与释放

内存分配 (mp_pool_alloc):

当请求内存时,函数遍历内存池,寻找首个具有足够空闲块的池。如果找到,它会更新该池的 free_ptr 指向下一个空闲块,并递减 free_count

void *mp_pool_alloc(mp_pool_t *mp, int size) {
    if (!mp) {
        fprintf(stderr, "Error: Memory pool structure is NULL.\n");
        return NULL;
    }
    for (int i = 0; i < NUM_SIZES; i++) {
        mempool_t *pool = &mp->pools[i];
        if (size <= pool->block_size && pool->free_count > 0) {
            void *ptr = pool->free_ptr;
            pool->free_ptr = *(char **)ptr;
            pool->free_count--;
            return ptr;
        }
    }
    fprintf(stderr, "Error: Memory pool allocation failed for size %d.\n", size);
    return NULL;
}

内存释放 (mp_pool_free):

释放内存时,函数检查每个池,找到包含指定 ptr 的池。它将内存块链接回该池的空闲链表,同时增加 free_count

void mp_pool_free(mp_pool_t *mp, void *ptr) {
    if (!mp || !ptr) return;
    for (int i = 0; i < NUM_SIZES; i++) {
        mempool_t *pool = &mp's.pools[i];
        char *start = pool->mem;
        char *end = start + MEM_PAGE_SIZE;
        if (ptr >= (void *)start && ptr < (void *)end) {
            *(char **)ptr = pool->free_ptr;
            pool->free_ptr = (char *)ptr;
            pool->free_count++;
            return;
        }
    }
    printf("Pointer not managed by any pool\n");
}

总结

该内存池设计实现了内存管理的基础功能,包括内存的预分配、快速分配与释放,并有效地通过链表结构管理内存块。这种设计有助于减少碎片,提高内存操作的效率,并提供更可预测的性能表现,特别适合于需要频繁分配和释放内存的场景。

4. 使用案例

以下代码是我在KV存储项目中用到内存池分配的代码,可以发现非常好理解,就是将原来malloc分配内存和free释放内存处进行替换成我们自己的接口就好,因为我们的接口对malloc、free这两个库函数都进行了封装。就类似CPP中的new和delete,这两个运算符,都是对malloc和free的重载。

void *kvstore_malloc(size_t size){
#if ENABLE_MEM_POOL 
	return mp_pool_alloc(&m,size);
#else
    return malloc(size);
#endif
}

void kvstore_free(void *ptr){
#if ENABLE_MEM_POOL
	mp_pool_free(&m,ptr);
#else
    return free(ptr);
#endif
}

5. 问题与限制

局限性
  • 预设块大小:内存池需要预先定义块的大小,这可能不适用于所有类型的应用,尤其是那些对内存大小需求高度不规则的应用。
  • 内存浪费:对于分配需求小于块大小的情况,内存池可能导致内存浪费。
  • 初始载入成本:内存池的初始化可能需要较大的初始内存载入,这在内存资源极其有限的环境下可能是一个缺点。
改进方向
  • 动态块大小调整:开发更智能的内存池,能够根据应用的实际需求动态调整块的大小。
  • 多级内存池:实现多级内存池,以支持更多种类和大小的内存分配需求。

6. 结论

内存池是一个强大的工具,它通过预分配和管理内存来提供更快的分配速度和减少运行时内存碎片。这使得内存池特别适用于需要频繁分配和释放内存的应用,如网络服务器、实时系统和高性能计算应用。通过使用内存池,开发者可以更好地控制应用程序的内存使用,优化性能并减少延迟。尽管有其局限性,适当的设计和改进可以使内存池成为高效管理内存的关键工具。

内存碎片

外部碎片

定义: 外部碎片是指不连续的空闲内存空间分散在整个内存中,尽管总空闲内存可能足够某次分配请求,但因为这些空闲内存不是连续的,无法满足需要连续内存块的分配请求。

传统管理下的外部碎片: 在使用如 mallocfree 这类通用内存分配函数时,应用程序可能在内存中留下许多大小不一的空洞。随着时间的推移,内存中可能会散布许多小的、不连续的空闲块,导致无法为较大的内存分配请求提供服务,即便总的空闲内存量实际上是足够的。

内存池的优势: 内存池通过预分配一大块内存并固定地将其划分为块来避免外部碎片。所有的内存块都有相同的大小,或者是预定义的几种大小。当内存被释放时,它会被归还到相应大小的内存池中,这些内存块可以立即被重新分配,而不会留下小的、不连续的空间。

内部碎片

定义: 内部碎片是指分配给程序的内存块中未被利用的部分。这通常发生在分配的内存块比实际需要的内存稍大时。

传统管理下的内部碎片: 如果使用 malloc 时请求的内存大小不是内存分配粒度的整数倍,分配的内存块通常会比请求的大小大一些。这种大小的差异就构成了内部碎片。

内存池的优势: 在内存池中,尽管内部碎片仍然存在,但通过选择合理的块大小和粒度来最小化它。例如,如果大多数对象都可以适应几个固定大小的块,则可以通过精心设计这些块的大小来最大限度地减少未使用的内存。

总结

内存池通过固定大小的块减少内存碎片,这简化了内存管理,并提高了性能。它特别适用于那些对象生命周期短、分配释放频繁的场景。此外,内存池的这种管理方式还可以提前计算出内存使用量,有助于避免因内存碎片导致的程序运行错误。

  • 36
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值