从零构建数据库内存管理模块:一次渐进式优化之旅
在数据库开发中,高效的内存管理是性能优化的关键。本文将分享我们在 miniToadb 项目中,如何从零开始构建一个完整的内存管理模块,通过多次迭代逐步优化的过程。
项目背景
miniToadb 是一个轻量级数据库项目,随着功能扩展,我们发现直接使用系统的 malloc/free 存在以下问题:
内存碎片严重 频繁的小内存分配导致性能下降 内存泄漏难以追踪 缺乏统一的内存管理策略
因此,我们决定开发一个专门的内存管理模块。
第一版:基础内存池(Memory Pool)
设计思路
第一版的核心目标是统一管理内存生命周期。我们引入"内存上下文(MemoryContext)"的概念,所有内存分配都关联到一个上下文,当上下文销毁时,自动释放所有关联的内存。
核心数据结构
// 内存块结构
typedef struct MemoryBlock
{
void* ptr; // 指向分配的内存
size_t size; // 内存块大小
struct MemoryBlock* next; // 链表指针
} MemoryBlock;
// 内存上下文
typedef struct MemoryContext
{
MemoryBlock* blocks; // 已分配内存块链表
} MemoryContext;
接口设计
// 创建内存上下文
MemoryContext* create_memory_context();
// 销毁内存上下文(释放所有关联内存)
void destroy_memory_context(MemoryContext* ctx);
// 从上下文中分配内存
void* memory_alloc(MemoryContext* ctx, size_t size);
// 释放内存
void memory_free(MemoryContext* ctx, void* ptr);
使用方式
// 程序启动时创建全局内存上下文
MemoryContext* g_memory_ctx = create_memory_context();
// 使用内存管理接口分配内存
void* data = memory_alloc(g_memory_ctx, 1024);
// 程序结束时统一释放
destroy_memory_context(g_memory_ctx);
第一版的特点
优点:
实现了内存的集中管理 避免了内存泄漏 代码结构清晰
不足:
每次分配都需要 malloc 两次(MemoryBlock + 用户数据) 内存碎片问题仍然存在 频繁的小内存分配性能不佳
第二版:统一内存块分配
优化思路
第二版的核心优化是将 MemoryBlock 和用户数据合并为一次分配,减少 malloc 调用次数,提高内存局部性。
关键改进
void* memory_alloc(MemoryContext* ctx, size_t size)
{
// 计算总大小:MemoryBlock + 用户数据
size_t total_size = sizeof(MemoryBlock) + size;
// 一次性分配
void* block_memory = malloc(total_size);
// MemoryBlock放在内存块头部
MemoryBlock* block = (MemoryBlock*)block_memory;
// 用户数据紧随其后
void* user_memory = (char*)block_memory + sizeof(MemoryBlock);
block->ptr = user_memory;
block->size = size;
block->next = ctx->blocks;
ctx->blocks = block;
return user_memory;
}
释放时的处理
void memory_free(MemoryContext* ctx, void* ptr)
{
// 通过ptr找到MemoryBlock(在ptr之前)
MemoryBlock* block = (MemoryBlock*)((char*)ptr - sizeof(MemoryBlock));
// 从链表中移除
// ...
// 一次性释放整个块
free(block);
}
第二版的提升
性能提升:
malloc 调用次数减少 50% 内存局部性更好,缓存命中率提高 减少了内存碎片
代码复杂度:
实现复杂度略有增加 需要小心处理指针偏移
第三版:内存对齐优化
优化思路
为了进一步提升性能和内存利用率,我们引入2 的幂次方对齐策略。内存块大小对齐到 2 的幂次方,便于后续的内存复用。
对齐算法
// 计算下一个2的幂次方
static size_t next_power_of_2(size_t size)
{
if (size == 0) return 1;
size--;
size |= size >> 1;
size |= size >> 2;
size |= size >> 4;
size |= size >> 8;
size |= size >> 16;
size++;
return size;
}
分配策略
void* memory_alloc(MemoryContext* ctx, size_t size)
{
size_t total_size = sizeof(MemoryBlock) + size;
// 对齐到2的幂次方
size_t aligned_size = next_power_of_2(total_size);
void* block_memory = malloc(aligned_size);
// ...
block->actual_size = aligned_size; // 记录实际大小
// ...
}
第三版的优势
内存管理:
内存块大小标准化,便于分类管理 减少了不同大小内存块的碎片 为后续的内存复用打下基础
第四版:空闲内存复用(Free List)
优化思路
频繁分配和释放小内存块会导致性能问题。第四版引入**空闲列表(Free List)**机制,释放的内存块不立即归还给系统,而是缓存起来供后续复用。
数据结构升级
typedef struct MemoryContext
{
MemoryBlock* blocks; // 已分配内存块链表
MemoryBlock* free_blocks; // 空闲内存块链表
size_t threshold; // 复用阈值
} MemoryContext;
内存分配流程
void* memory_alloc(MemoryContext* ctx, size_t size)
{
// 1. 先在空闲列表中查找合适的块
MemoryBlock** free_ptr = &ctx->free_blocks;
while (*free_ptr)
{
if ((*free_ptr)->size >= size)
{
// 找到合适的空闲块,直接复用
MemoryBlock* block = *free_ptr;
*free_ptr = block->next;
// 移到已分配链表
block->next = ctx->blocks;
ctx->blocks = block;
return block->ptr;
}
free_ptr = &(*free_ptr)->next;
}
// 2. 没有找到,分配新内存
// ... 调用malloc
}
内存释放流程
void memory_free(MemoryContext* ctx, void* ptr)
{
// 从已分配链表移除
// ...
// 加入空闲链表(不真正释放)
block->next = ctx->free_blocks;
ctx->free_blocks = block;
}
第四版的性能提升
测试数据:
小内存分配性能提升约 60% 减少了系统调用次数 内存碎片显著减少
第五版:分级空闲列表(Size-Class Based Free List)
优化思路
单链表的空闲列表在查找时需要遍历所有块。第五版采用分级空闲列表,按内存块大小分类存储,实现 O(1)的查找效率。
核心设计
#define MAX_SIZE_CLASS 20 // 最大支持2^20字节
typedef struct MemoryContext
{
MemoryBlock* blocks;
MemoryBlock* free_blocks[MAX_SIZE_CLASS]; // 分级空闲列表
size_t threshold;
} MemoryContext;
大小类计算
// 根据大小计算所属的大小类
static int find_size_class(size_t size)
{
int class = 0;
size_t current_size = 1;
while (current_size < size && class < MAX_SIZE_CLASS - 1)
{
current_size <<= 1;
class++;
}
return class;
}
分配和释放优化
void* memory_alloc(MemoryContext* ctx, size_t size)
{
size_t actual_size = next_power_of_2(sizeof(MemoryBlock) + size);
int size_class = find_size_class(actual_size);
// 直接从对应大小类的列表中取
if (ctx->free_blocks[size_class])
{
MemoryBlock* block = ctx->free_blocks[size_class];
ctx->free_blocks[size_class] = block->next;
// ...
return block->ptr;
}
// 没有则分配新内存
// ...
}
void memory_free(MemoryContext* ctx, void* ptr)
{
// ...
int size_class = find_size_class(block->actual_size);
// 放回对应大小类的列表
block->next = ctx->free_blocks[size_class];
ctx->free_blocks[size_class] = block;
}
第五版的突破
性能飞跃:
内存分配接近 O(1)时间复杂度 查找空闲块无需遍历 内存碎片进一步减少
第六版:大内存直接释放策略
优化思路
缓存所有内存块可能导致内存占用过高。第六版引入阈值策略:超过一定大小的内存块直接释放,不加入空闲列表。
实现细节
void memory_free(MemoryContext* ctx, void* ptr)
{
// ...
// 大内存直接释放
if (block->size > ctx->threshold)
{
free(block); // 直接归还给系统
}
else
{
// 小内存加入空闲列表复用
int size_class = find_size_class(block->actual_size);
block->next = ctx->free_blocks[size_class];
ctx->free_blocks[size_class] = block;
}
}
阈值选择
MemoryContext* create_memory_context()
{
MemoryContext* ctx = malloc(sizeof(MemoryContext));
// ...
ctx->threshold = 4096; // 4KB作为阈值
// ...
}
第六版的平衡
内存与性能的平衡:
小内存(<4KB):缓存复用,提高性能 大内存(>=4KB):直接释放,减少内存占用 根据应用场景可调整阈值
迭代演进总结
使用建议
1. 初始化
// 程序启动时创建全局内存上下文
MemoryContext* g_memory_ctx = create_memory_context();
2. 内存分配
// 使用memory_alloc代替malloc
void* data = memory_alloc(g_memory_ctx, size);
3. 内存释放
// 使用memory_free代替free
memory_free(g_memory_ctx, data);
4. 程序退出
// 统一释放所有内存
destroy_memory_context(g_memory_ctx);
总结
内存管理模块的开发是一个渐进式优化的过程。从最初的基础内存池,到最终的分级空闲列表,每一次迭代都针对特定的问题进行优化。关键是要:
先解决正确性问题(避免内存泄漏) 再解决性能问题(减少分配开销) 最后解决资源问题(平衡内存占用)
这种渐进式开发方式不仅降低了开发难度,也使得每一步的优化都有据可依。希望本文的分享对您的项目有所帮助!
本文代码基于 miniToadb 项目,完整代码可在后台咨询获取。
文章转载自开源无限,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




