自学内容网 自学内容网

linux网络编程10——内存池

内存池

本文讲述了内存池的概念、原理、好处,并且实现了两种类型的内存池:固定大小的内存池和分级内存池。最后分别对这两种内存进行了性能测试,测试结果表明,在简单的频繁分配和释放动态内存块的场景下,这两种内存池至少能够提高一倍的性能。

1. 内存池概念

内存池定义

内存池(Memory Pool)是一种用于优化内存分配和释放的技术,通过预先分配一块较大的内存区域,并将其划分为多个小块,在需要时直接从这块区域中分配内存,而不必每次都向操作系统申请或释放内存。

使用内存池的好处

  • 减少频繁动态内存分配的开销
  • 减少内存碎片

问题:进程长期运行后随机出现core dump问题?

大概率是由于内存碎片过多或者内存泄漏,导致oom问题。

2. 固定大小内存块的内存池

这种类型的内存池会预先分配一大块内存,并将其划分为多个固定大小的小块。然后使用链表管理这些小块,每次分配时从链表头返回一个块,释放时将块重新加入链表。

被释放的内存如何被再次使用?

用一个链表将空闲块串起来,当内存块被释放时就重新插入空闲块链。

如何保存空闲块链的next指针?

可以直接在空闲块的头部前8个字节保存。

如何扩展内存池

仍然可以采用链表将已经申请的大内存块连接起来。至于next指针,可以将其保存在大内存块的头部作为”元信息“,并进行指针偏移。

在这里插入图片描述

上图展示了固定大小内存块的内存池的基本结构,一共有两个链表,分别是已经申请的大块的链表和空闲的小内存块的链表。

下面通过具体的代码来学习该内存池的实现。

2.1 内存池创建

首先需要设计内存池控制块的数据结构:

typedef struct _memory_pool
{
    void *head;
    void *idles;
    size_t large_block_num;
    size_t size;
} memory_pool;
  • head:指向预先分配的大块链表头节点,主要用于在内存池销毁时释放内存,防止内存泄漏。

  • idles:指向空闲的小内存块链表的头节点,当用户申请或释放内存块时,就像其中弹出或插入内存块。

  • large_block_num:大内存块的总数量,能够表示当前内存池占用内存大小。

  • size:小内存块的尺寸,是一个固定值,可在创建时设置。

有个这个结构体,我们就可以开始创建内存池了:

// 预先分配一个大内存块
static int alloc_new_block(memory_pool *pool)
{
    if (!pool) return -1;
    char *block = malloc(LARGE_BLOCK_SIZE + POINTER_SIZE);
    if (!block) return -2;

    *(char **)block = pool->head;
    pool->head = block;
    ++pool->large_block_num;

    block += POINTER_SIZE;

    size_t size = pool->size;
    char *p = pool->idles;
    for (size_t cur = 0; cur + size <= LARGE_BLOCK_SIZE; cur += size)
    {
        *(char **)(block + cur) = p;
        p = block + cur;
    }
    pool->idles = p;

    return 0;
}

// 创建内存池,如果失败返回NULL
memory_pool *create_memory_pool(size_t size)
{
    if (size > LARGE_BLOCK_SIZE || size < POINTER_SIZE) return NULL;

    memory_pool *pool = malloc(sizeof(memory_pool));
    if (!pool)
        return NULL;

    memset(pool, 0, sizeof(memory_pool));
    pool->size = size;
    
    if (alloc_new_block(pool) != 0)
        return NULL;
    return pool;
}

注意点:

  1. 在create_memory_pool函数中,创建内存池的同时预先分配一个大块内存。
  2. alloc_new_block函数负责执行实际的分配任务,并将大块内存划分成小内存并加入到idles链表中。大内存块本身也被加入到了head链表中,其中隐藏的前8个字节用于保存指针域,因此你会看到 char *block = malloc(LARGE_BLOCK_SIZE + POINTER_SIZE);这条代码额外申请了POINTER_SIZE的内存。

2.2 内存池的销毁

销毁就只需要遍历head链表即可,比较简单

void destroy_memory_pool(memory_pool *pool)
{
    void *head = pool->head;
    while (head)
    {
        void *next = *(char **)head;
        free(head);
        head = next;
    }
    free(pool);
}

2.3 分配内存块

主要工作是从idles中取出一个小内存块,如果iele为空,就调用alloc_new_block再分配一个大内存块。

void *memory_alloc(memory_pool *pool)
{
    if (!pool) return NULL;

    if (pool->idles == NULL)
    {
        if (alloc_new_block(pool) != 0)
            return NULL;
    }
    assert(pool->idles != NULL);

    void *ret = pool->idles;
    pool->idles = *(void **)pool->idles;

    return ret;
}

2.4 释放内存块

主要工作是将小内存块插入idles中,比较简单。

void memory_free(memory_pool *pool, void *block)
{
    if (!pool || !block) return;

    *(void **)block = pool->idles;
    pool->idles = block;
}

3. 固定内存块性能测试

3.1 测试用例

下面的测试用例将测试不同尺寸的小内存块的分配和释放性能,同时与不使用内存池的情况进行对比。

#include <stdio.h>

#include "solid_pool.h"

#include <sys/time.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#define BLOCK_SIZE 128
#define BLOCK_NUMS 10000
#define TURNS 1000

void alloc_and_free(memory_pool *pool, char **blocks, int size)
{
    for (int i = 0; i < size; ++i)
    {
        blocks[i] = memory_alloc(pool);
        // snprintf(blocks[i], BLOCK_SIZE, "the message from %d ..............", i + 1);
    }

    // printf("memory_alloc done\n");

    for (int i = 0; i < size; ++i)
    {
        // printf("%s\n", blocks[i]);
        memory_free(pool, blocks[i]);
    }
}

int testcase()
{
    char *blocks[BLOCK_NUMS];

    memory_pool *pool = create_memory_pool(BLOCK_SIZE);
    // printf("create_memory_pool done\n");

    if (pool == NULL) return -1;

    for (int i = 0; i < TURNS; ++i)
        alloc_and_free(pool, blocks, BLOCK_NUMS);

    // printf("memory_free done\n");

    destroy_memory_pool(pool);

    // printf("destroy_memory_pool done\n");
    return 0;
}

void alloc_and_free_no_pool(char **blocks, int size)
{
    for (int i = 0; i < size; ++i)
    {
        blocks[i] = malloc(BLOCK_SIZE);
        // snprintf(blocks[i], BLOCK_SIZE, "the message from %d ..............", i + 1);
    }

    // printf("memory_alloc done\n");

    for (int i = 0; i < size; ++i)
    {
        // printf("%s\n", blocks[i]);
        free(blocks[i]);
    }
}

int testcase_no_pool()
{
    char *blocks[BLOCK_NUMS];
    for (int i = 0; i < TURNS; ++i)
        alloc_and_free_no_pool(blocks, BLOCK_NUMS);
    return 0;
}

int main(int argc, char const *argv[])
{
    struct timeval begin;
    struct timeval end;
    memset(&begin, 0, sizeof begin);
    memset(&end, 0, sizeof end);

    gettimeofday(&begin, NULL);

    testcase();

    gettimeofday(&end, NULL);

    int interval_ms = (end.tv_sec - begin.tv_sec) * 1000 + (end.tv_usec - begin.tv_usec) / 1000;
    printf("with pool, total interval: %dms\n", interval_ms);
    

    memset(&begin, 0, sizeof begin);
    memset(&end, 0, sizeof end);

    gettimeofday(&begin, NULL);

    testcase_no_pool();

    gettimeofday(&end, NULL);

    interval_ms = (end.tv_sec - begin.tv_sec) * 1000 + (end.tv_usec - begin.tv_usec) / 1000;
    printf("without pool, total interval: %dms\n", interval_ms);


    return 0;
}

3.2 测试结果

每轮测试申请和释放的内存块数量:10000

每次测试申请和释放的轮数:1000

内存块大小使用内存池耗时(ms)不适用内存池耗时(ms)
1669129
3271136
6484142
128113638
2561281017
5121331772

结论:当内存申请和释放次数较多时,使用内存池性能至少提升一倍,并且随着内存块大大小的增加,性能提升幅度也更大。

4. 分级内存池

分级内存池与固定内存池的不同在于,前者可以分配不同大小的内存块,而后者只能分配固定大小的内存块。一般来说,为了管理不同大小的内存块,分级内存池需要维护多个不同层级的内存池,每个层级的内存池负责一种尺寸的内存块的管理。这些内存池可以用链表实现,也可以用数组实现。

下面我们来实现一个经典的使用多级自由链表实现的分级内存池。

在这里插入图片描述

4.1 内存池创建

同固定大小内存池类似,先来设计以下内存池控制块的结构体。

首先,还是要有一个链表来保存已经预先分配的大块内存,可以为其取名chunks。然后需要一个数组来保存多级链表的头节点指针。

与固定大小内存池不同的是,由于我们不知道预先分配的大块内存会被划分成多小的块,所以不能再分配时就把其划分成小块加入自由链表,而应该在用户allocate时再分配。这样的话就需要保存一个指针指向待分配的内存首地址,以及一个整数保存大内存块中还剩下多少内存可被分配。

结构体的实现如下:

#define LARGE_BLOCK_SIZE 4096
#define INTERVAL 16
#define LAYER_NUM 32
#define POINTER_SIZE sizeof(void*)

typedef struct _tiered_memory_pool
{
    void *chunks;
    void *idle_chunk;
    size_t chunk_size;
    void *free_list[LAYER_NUM];
} tiered_memory_pool;

其中,INTERVAL指的是相邻级别的内存池保存的内存块大小的间隔,LATYER_NUM指的是总共有多少个级别。

接下来,要创建一个内存池,仍然预先分配一个内存块,代码如下:

static int allocate_new_chunk(tiered_memory_pool *pool)
{
    if (!pool) return -1;

    void *chunk = malloc(LARGE_BLOCK_SIZE + POINTER_SIZE);
    if (!chunk) return -2;

    // try using the remaining idle memory in idle_chunk
    int layer = round_down(pool->chunk_size);
    assert(layer < LAYER_NUM);
    if (layer >= 0)
    {
        void *idle = pool->idle_chunk;
        *(void **)idle = pool->free_list[layer];
        pool->free_list[layer] = idle;
    }

    *(void **)chunk = pool->chunks;
    pool->chunks = chunk;
    pool->idle_chunk = chunk + POINTER_SIZE;
    pool->chunk_size = LARGE_BLOCK_SIZE;

    return 0;
}

注意看,分配大内存块的函数与固定内存池的相应函数主要有两点不同:

  1. 不立即将大内存块划分并填入自由链表
  2. 在分配之前会将就得大内快中剩下的(但无法满足本次分配的)内存尝试填入自由链表中。

第二步是为了减少内存的浪费。想象一下,当大内存块中还剩下256字节,这有一个分配需求是400字节,那么就需要重新申请一个大内存块进行分配,这时剩下的这256字节就会被浪费。为了避免浪费,我们可以检查下,还剩下多少空闲内存,并把它插入合适的自由链表。

最后是供用户使用的创建内存的函数:

tiered_memory_pool *create_memory_pool()
{
    tiered_memory_pool *pool = calloc(1, sizeof(tiered_memory_pool));
    if (pool == NULL) return NULL;

    allocate_new_chunk(pool);
    return pool;
}

4.2 内存池的销毁

该操作与固定大小内存池的销毁类似,不再多说。

void destroy_memory_pool(tiered_memory_pool *pool)
{
    if (!pool) return;
    void *cur = pool->chunks;
    while (cur != NULL)
    {
        void *temp = cur;
        cur = *(void **)cur;
        free(temp);
    }
    free(pool);
}

4.3 分配内存块

由于内存池的级别有限,大于一定尺寸的内存块是无法进行管理的,这时需要直接使用系统malloc和free函数。总体代码如下:

void *memory_alloc(tiered_memory_pool *pool, size_t size)
{
    if (!pool || size < POINTER_SIZE) return NULL;

    int layer = round_up(size);
    if (layer >= LAYER_NUM)
        return malloc(size);

    void *res = pool->free_list[layer];
    
    if (res != NULL)
    {
        pool->free_list[layer] = *(void **)res;
        return res;
    }

    if (pool->chunk_size < size)
        allocate_new_chunk(pool);

    res = pool->idle_chunk;
    pool->chunk_size -= size;
    pool->idle_chunk += size;
    return res;
}

4.4 释放内存块

主要操作是检查内存块的大小是否可被管理,如何可以就找到对应的自由链表,并使用头插法插入其中。

void memory_free(tiered_memory_pool *pool, void *ptr, size_t size)
{
    if (!pool || !ptr || size < POINTER_SIZE) return;

    int layer = round_up(size);
    if (layer >= LAYER_NUM)
    {
        free(ptr);
        return;
    }

    *(void **)ptr = pool->free_list[layer];
    pool->free_list[layer] = ptr;
}

5. 分级内存池性能测试

5.1 测试用例

为了模拟,不同大小内存块的分配,我们使用伪随机算法生成内存块的大小。

测试的代码如下:

#include "tiered_pool_testcase.h"

#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <stdio.h>

#include "tiered_pool.h"

int testcase()
{
    tiered_memory_pool *pool = create_memory_pool();

    if (!pool) return -1;

    void *blocks[MAX_ALLOCTE];
    size_t lens[MAX_ALLOCTE];

    for (int k = 0; k < TURN; ++k)
    {
        for (int i = 0; i < MAX_ALLOCTE; ++i)
        {
            int size = (rand() % (TEST_MAX_SIZE - TEST_MIN_SIZE)) + TEST_MIN_SIZE;
            blocks[i] = memory_alloc(pool, size);
            lens[i] = size;
        }

        for (int i = 0; i < MAX_ALLOCTE; ++i)
        {
            memory_free(pool, blocks[i], lens[i]);
        }
    }

    destroy_memory_pool(pool);

    return 0;
}

int main(int argc, char const *argv[])
{
    struct timeval begin, end;
    memset(&begin, 0, sizeof begin);
    memset(&end, 0, sizeof end);

    gettimeofday(&begin, NULL);

    testcase();

    gettimeofday(&end, NULL);

    int interval = (end.tv_sec - begin.tv_sec) * 1000 + (end.tv_usec - begin.tv_usec) / 1000;

    printf("with memory pool, total interval: %dms\n", interval);
    
    return 0;
}

5.2 测试结果

最小内存块尺寸最大内存块尺寸使用内存池耗时(ms)不使用内存池耗时(ms)
8512218411

结论:使用内存池性能大约提升一倍。

学习参考

学习更多相关知识请参考零声 github


原文地址:https://blog.csdn.net/leedcanDD/article/details/143861269

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!