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;
}
注意点:
- 在create_memory_pool函数中,创建内存池的同时预先分配一个大块内存。
- 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) |
---|---|---|
16 | 69 | 129 |
32 | 71 | 136 |
64 | 84 | 142 |
128 | 113 | 638 |
256 | 128 | 1017 |
512 | 133 | 1772 |
结论:当内存申请和释放次数较多时,使用内存池性能至少提升一倍,并且随着内存块大大小的增加,性能提升幅度也更大。
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;
}
注意看,分配大内存块的函数与固定内存池的相应函数主要有两点不同:
- 不立即将大内存块划分并填入自由链表
- 在分配之前会将就得大内快中剩下的(但无法满足本次分配的)内存尝试填入自由链表中。
第二步是为了减少内存的浪费。想象一下,当大内存块中还剩下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) |
---|---|---|---|
8 | 512 | 218 | 411 |
结论:使用内存池性能大约提升一倍。
学习参考
学习更多相关知识请参考零声 github。
原文地址:https://blog.csdn.net/leedcanDD/article/details/143861269
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!