暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【手写数据库系列】从零构建数据库内存管理模块:一次渐进式优化之旅

开源无限 2026-03-13
51

从零构建数据库内存管理模块:一次渐进式优化之旅

在数据库开发中,高效的内存管理是性能优化的关键。本文将分享我们在 miniToadb 项目中,如何从零开始构建一个完整的内存管理模块,通过多次迭代逐步优化的过程。

项目背景

miniToadb 是一个轻量级数据库项目,随着功能扩展,我们发现直接使用系统的 malloc/free 存在以下问题:

  • 内存碎片严重
  • 频繁的小内存分配导致性能下降
  • 内存泄漏难以追踪
  • 缺乏统一的内存管理策略

因此,我们决定开发一个专门的内存管理模块。


第一版:基础内存池(Memory Pool)

设计思路

第一版的核心目标是统一管理内存生命周期。我们引入"内存上下文(MemoryContext)"的概念,所有内存分配都关联到一个上下文,当上下文销毁时,自动释放所有关联的内存。

核心数据结构

// 内存块结构
typedef struct MemoryBlock
{

    void* ptr;              // 指向分配的内存
    size_t size;            // 内存块大小
    struct MemoryBlocknext; // 链表指针
} MemoryBlock;

// 内存上下文
typedef struct MemoryContext
{

    MemoryBlock* blocks;     // 已分配内存块链表
} MemoryContext;

接口设计

// 创建内存上下文
MemoryContext* create_memory_context();

// 销毁内存上下文(释放所有关联内存)
void destroy_memory_context(MemoryContext* ctx);

// 从上下文中分配内存
voidmemory_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 调用次数,提高内存局部性。

关键改进

voidmemory_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 == 0return 1;

    size--;
    size |= size >> 1;
    size |= size >> 2;
    size |= size >> 4;
    size |= size >> 8;
    size |= size >> 16;
    size++;

    return size;
}

分配策略

voidmemory_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;

内存分配流程

voidmemory_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;
}

分配和释放优化

voidmemory_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):直接释放,减少内存占用
  • 根据应用场景可调整阈值

迭代演进总结

版本
核心特性
解决的问题
性能提升
V1
基础内存池
内存泄漏
基础功能
V2
统一分配
减少 malloc 次数
30%
V3
2 的幂对齐
内存碎片
20%
V4
空闲列表
内存复用
60%
V5
分级空闲列表
查找效率
80%
V6
大内存直释
内存占用
平衡优化

使用建议

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);


总结

内存管理模块的开发是一个渐进式优化的过程。从最初的基础内存池,到最终的分级空闲列表,每一次迭代都针对特定的问题进行优化。关键是要:

  1. 先解决正确性问题(避免内存泄漏)
  2. 再解决性能问题(减少分配开销)
  3. 最后解决资源问题(平衡内存占用)

这种渐进式开发方式不仅降低了开发难度,也使得每一步的优化都有据可依。希望本文的分享对您的项目有所帮助!


本文代码基于 miniToadb 项目,完整代码可在后台咨询获取。



文章转载自开源无限,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论