教程 06 - 内存子系统

💾 构建可追踪、可调试的内存管理系统

关注公众号【上手实验室】,领取章节视频教程


📋 目录


🎯 本章概述

📚 你将学到

✅ 自定义内存管理的重要性
✅ 内存标签(Memory Tag)系统
✅ 内存分配追踪机制
✅ 内存统计和报告
✅ 内存泄漏检测方法
✅ 平台内存 API 的封装

🔧 涉及文件

📄 engine/src/core/kmemory.h (41 行)
📄 engine/src/core/kmemory.c (111 行)
📄 engine/src/entry.h (更新)
📄 engine/src/core/application.c (更新)
📄 engine/src/platform/platform.h (更新)
📄 testbed/src/entry.c (更新)

Git Commit: 5f842d4


🤔 为什么需要内存子系统

❌ 直接使用 malloc/free 的问题

// ❌ 传统方式
void* data = malloc(1024);
// ... 使用 data ...
free(data);

存在的问题:

问题说明影响
❌ 无法追踪不知道谁分配了内存内存泄漏难以定位
❌ 无统计信息不知道使用了多少内存无法优化内存使用
❌ 未初始化malloc 不会清零内存可能读取随机数据
❌ 平台差异不同平台的内存管理不同跨平台问题
❌ 调试困难无法在运行时检查内存调试效率低

✅ 自定义内存子系统的优势

╔════════════════════════════════════════════════════════╗
║  🎯 内存子系统的核心优势                                ║
╠════════════════════════════════════════════════════════╣
║  1️⃣ 分类追踪(Memory Tagging)                        ║
║     • 标记内存属于哪个子系统(纹理、模型、游戏等)     ║
║     • 统计各子系统的内存使用情况                       ║
║     • 快速定位内存泄漏源头                             ║
║                                                        ║
║  2️⃣ 自动清零(Auto Zeroing)                          ║
║     • 所有新分配的内存自动清零                         ║
║     • 避免未初始化的内存导致的 bug                     ║
║     • 更安全的内存使用                                 ║
║                                                        ║
║  3️⃣ 统计报告(Statistics)                            ║
║     • 实时查看内存使用情况                             ║
║     • 生成详细的内存报告                               ║
║     • 优化内存分配策略                                 ║
║                                                        ║
║  4️⃣ 平台抽象(Platform Abstraction)                  ║
║     • 统一的内存管理接口                               ║
║     • 隐藏平台特定实现                                 ║
║     • 未来可以替换为自定义分配器                       ║
║                                                        ║
║  5️⃣ 调试支持(Debug Support)                         ║
║     • 检测内存泄漏                                     ║
║     • 检测重复释放                                     ║
║     • 检测越界访问(未来扩展)                         ║
╚════════════════════════════════════════════════════════╝

🏗️ 架构设计

📊 整体架构图

🖥️ 操作系统
⚙️ 平台层
🔧 引擎层
🎮 游戏层
kallocate
kfree
kallocate
kfree
kallocate
kfree
platform_*
系统调用
malloc/free
VirtualAlloc/Free
mmap/munmap
platform_allocate
platform_free
platform_zero_memory
kmemory.h/c
内存子系统
application.c
其他子系统
游戏代码

🔄 内存分配流程

游戏代码 kmemory platform 操作系统 kallocate(1024, MEMORY_TAG_GAME) 更新统计信息 stats.tagged_allocations[GAME] += 1024 platform_allocate(1024, FALSE) malloc(1024) 返回内存块 返回内存块 platform_zero_memory(block, 1024) 清零完成 返回已清零的内存块 使用内存... kfree(block, 1024, MEMORY_TAG_GAME) 更新统计信息 stats.tagged_allocations[GAME] -= 1024 platform_free(block, FALSE) free(block) 游戏代码 kmemory platform 操作系统

💻 核心代码分析

🏷️ 内存标签定义 (kmemory.h)

📄 kmemory.h 完整源码
#pragma once

#include "defines.h"

typedef enum memory_tag {
    // For temporary use. Should be assigned one of the below or have a new tag created.
    MEMORY_TAG_UNKNOWN,
    MEMORY_TAG_ARRAY,
    MEMORY_TAG_DARRAY,
    MEMORY_TAG_DICT,
    MEMORY_TAG_RING_QUEUE,
    MEMORY_TAG_BST,
    MEMORY_TAG_STRING,
    MEMORY_TAG_APPLICATION,
    MEMORY_TAG_JOB,
    MEMORY_TAG_TEXTURE,
    MEMORY_TAG_MATERIAL_INSTANCE,
    MEMORY_TAG_RENDERER,
    MEMORY_TAG_GAME,
    MEMORY_TAG_TRANSFORM,
    MEMORY_TAG_ENTITY,
    MEMORY_TAG_ENTITY_NODE,
    MEMORY_TAG_SCENE,

    MEMORY_TAG_MAX_TAGS
} memory_tag;

KAPI void initialize_memory();
KAPI void shutdown_memory();

KAPI void* kallocate(u64 size, memory_tag tag);

KAPI void kfree(void* block, u64 size, memory_tag tag);

KAPI void* kzero_memory(void* block, u64 size);

KAPI void* kcopy_memory(void* dest, const void* source, u64 size);

KAPI void* kset_memory(void* dest, i32 value, u64 size);

KAPI char* get_memory_usage_str();
🔬 内存标签枚举详解

memory_tag 枚举

typedef enum memory_tag {
    MEMORY_TAG_UNKNOWN,           // 未分类(临时使用,应避免)
    MEMORY_TAG_ARRAY,             // 静态数组
    MEMORY_TAG_DARRAY,            // 动态数组
    MEMORY_TAG_DICT,              // 字典/哈希表
    MEMORY_TAG_RING_QUEUE,        // 环形队列
    MEMORY_TAG_BST,               // 二叉搜索树
    MEMORY_TAG_STRING,            // 字符串
    MEMORY_TAG_APPLICATION,       // 应用层
    MEMORY_TAG_JOB,               // 任务系统
    MEMORY_TAG_TEXTURE,           // 纹理数据
    MEMORY_TAG_MATERIAL_INSTANCE, // 材质实例
    MEMORY_TAG_RENDERER,          // 渲染器
    MEMORY_TAG_GAME,              // 游戏逻辑
    MEMORY_TAG_TRANSFORM,         // 变换矩阵
    MEMORY_TAG_ENTITY,            // 实体对象
    MEMORY_TAG_ENTITY_NODE,       // 实体节点
    MEMORY_TAG_SCENE,             // 场景数据

    MEMORY_TAG_MAX_TAGS           // 标签总数(用于数组大小)
} memory_tag;

标签分类

类别标签用途
数据结构ARRAY静态数组分配
DARRAY动态数组(类似 std::vector)
DICT字典/哈希表
RING_QUEUE环形缓冲区
BST二叉搜索树
STRING字符串分配
引擎子系统APPLICATION应用层数据
JOB多线程任务
RENDERER渲染器数据
资源数据TEXTURE纹理内存(通常很大)
MATERIAL_INSTANCE材质实例
SCENE场景数据
游戏对象GAME游戏逻辑数据
ENTITY游戏实体
TRANSFORM变换数据

为什么需要 MEMORY_TAG_UNKNOWN

// ⚠️ 临时使用,后续应该改为具体标签
void* temp = kallocate(128, MEMORY_TAG_UNKNOWN);

// ✅ 正确做法:使用具体标签
void* game_data = kallocate(128, MEMORY_TAG_GAME);

使用 UNKNOWN 会触发警告:

[WARN]: kallocate called using MEMORY_TAG_UNKNOWN. Re-class this allocation.
📜 API 函数详解

1️⃣ 初始化和关闭

KAPI void initialize_memory();
KAPI void shutdown_memory();
函数说明调用时机
initialize_memory()初始化内存子系统,清零统计数据程序启动时(在 main 开头)
shutdown_memory()关闭内存子系统,检查泄漏程序退出时(在 main 结尾)

2️⃣ 内存分配和释放

KAPI void* kallocate(u64 size, memory_tag tag);
KAPI void kfree(void* block, u64 size, memory_tag tag);
参数类型说明
sizeu64分配/释放的字节数
tagmemory_tag内存标签(分类)
blockvoid*要释放的内存块指针

为什么 kfree 需要 size 参数?

// ⚠️ 标准 free 不需要 size
free(ptr);

// ✅ kfree 需要 size 用于统计
kfree(ptr, 1024, MEMORY_TAG_GAME);
//            ↑ 用于更新统计信息

3️⃣ 内存操作工具

KAPI void* kzero_memory(void* block, u64 size);
KAPI void* kcopy_memory(void* dest, const void* source, u64 size);
KAPI void* kset_memory(void* dest, i32 value, u64 size);
函数等价于说明
kzero_memorymemset(block, 0, size)清零内存
kcopy_memorymemcpy(dest, src, size)复制内存
kset_memorymemset(dest, value, size)设置内存

4️⃣ 统计报告

KAPI char* get_memory_usage_str();

返回值:格式化的内存使用报告字符串。

示例输出

System memory use (tagged):
  UNKNOWN    : 0.00B
  ARRAY      : 512.00B
  DARRAY     : 2.50KiB
  DICT       : 0.00B
  ...
  TEXTURE    : 16.75MiB
  RENDERER   : 4.20MiB
  GAME       : 1.15KiB

🛠️ 内存子系统实现 (kmemory.c)

📄 kmemory.c 完整源码
#include "kmemory.h"

#include "core/logger.h"
#include "platform/platform.h"

// TODO: Custom string lib
#include <string.h>
#include <stdio.h>

struct memory_stats {
    u64 total_allocated;
    u64 tagged_allocations[MEMORY_TAG_MAX_TAGS];
};

static const char* memory_tag_strings[MEMORY_TAG_MAX_TAGS] = {
    "UNKNOWN    ",
    "ARRAY      ",
    "DARRAY     ",
    "DICT       ",
    "RING_QUEUE ",
    "BST        ",
    "STRING     ",
    "APPLICATION",
    "JOB        ",
    "TEXTURE    ",
    "MAT_INST   ",
    "RENDERER   ",
    "GAME       ",
    "TRANSFORM  ",
    "ENTITY     ",
    "ENTITY_NODE",
    "SCENE      "};

static struct memory_stats stats;

void initialize_memory() {
    platform_zero_memory(&stats, sizeof(stats));
}

void shutdown_memory() {
}

void* kallocate(u64 size, memory_tag tag) {
    if (tag == MEMORY_TAG_UNKNOWN) {
        KWARN("kallocate called using MEMORY_TAG_UNKNOWN. Re-class this allocation.");
    }

    stats.total_allocated += size;
    stats.tagged_allocations[tag] += size;

    // TODO: Memory alignment
    void* block = platform_allocate(size, FALSE);
    platform_zero_memory(block, size);
    return block;
}

void kfree(void* block, u64 size, memory_tag tag) {
    if (tag == MEMORY_TAG_UNKNOWN) {
        KWARN("kfree called using MEMORY_TAG_UNKNOWN. Re-class this allocation.");
    }

    stats.total_allocated -= size;
    stats.tagged_allocations[tag] -= size;

    // TODO: Memory alignment
    platform_free(block, FALSE);
}

void* kzero_memory(void* block, u64 size) {
    return platform_zero_memory(block, size);
}

void* kcopy_memory(void* dest, const void* source, u64 size) {
    return platform_copy_memory(dest, source, size);
}

void* kset_memory(void* dest, i32 value, u64 size) {
    return platform_set_memory(dest, value, size);
}

char* get_memory_usage_str() {
    const u64 gib = 1024 * 1024 * 1024;
    const u64 mib = 1024 * 1024;
    const u64 kib = 1024;

    char buffer[8000] = "System memory use (tagged):\n";
    u64 offset = strlen(buffer);
    for (u32 i = 0; i < MEMORY_TAG_MAX_TAGS; ++i) {
        char unit[4] = "XiB";
        float amount = 1.0f;
        if (stats.tagged_allocations[i] >= gib) {
            unit[0] = 'G';
            amount = stats.tagged_allocations[i] / (float)gib;
        } else if (stats.tagged_allocations[i] >= mib) {
            unit[0] = 'M';
            amount = stats.tagged_allocations[i] / (float)mib;
        } else if (stats.tagged_allocations[i] >= kib) {
            unit[0] = 'K';
            amount = stats.tagged_allocations[i] / (float)kib;
        } else {
            unit[0] = 'B';
            unit[1] = 0;
            amount = (float)stats.tagged_allocations[i];
        }

        i32 length = snprintf(buffer + offset, 8000, "  %s: %.2f%s\n", memory_tag_strings[i], amount, unit);
        offset += length;
    }
    char* out_string = _strdup(buffer);
    return out_string;
}
🔬 实现细节分析

1️⃣ 内存统计结构

struct memory_stats {
    u64 total_allocated;                            // 总分配量
    u64 tagged_allocations[MEMORY_TAG_MAX_TAGS];   // 各标签分配量
};

static struct memory_stats stats;  // 全局单例

为什么使用全局变量?

原因说明
简化接口不需要在每个函数中传递状态指针
性能避免频繁的指针解引用
单例语义整个程序只有一个内存统计实例

2️⃣ 标签字符串表

static const char* memory_tag_strings[MEMORY_TAG_MAX_TAGS] = {
    "UNKNOWN    ",  // 11 个字符(包括填充空格)
    "ARRAY      ",
    "DARRAY     ",
    // ...
};

为什么使用固定宽度?

// ✅ 对齐输出
  UNKNOWN    : 0.00B
  ARRAY      : 512.00B
  DARRAY     : 2.50KiB

// ❌ 如果不对齐
  UNKNOWN: 0.00B
  ARRAY: 512.00B
  DARRAY: 2.50KiB  // 难以阅读

3️⃣ initialize_memory() 实现

void initialize_memory() {
    platform_zero_memory(&stats, sizeof(stats));
}

作用

  • 将所有统计数据清零
  • 准备内存追踪系统

为什么用 platform_zero_memory 而不是 memset

// ❌ 直接使用 memset
memset(&stats, 0, sizeof(stats));  // 平台依赖

// ✅ 使用平台层封装
platform_zero_memory(&stats, sizeof(stats));  // 跨平台

4️⃣ kallocate() 实现

void* kallocate(u64 size, memory_tag tag) {
    // ⚠️ 步骤 1: 检查标签
    if (tag == MEMORY_TAG_UNKNOWN) {
        KWARN("kallocate called using MEMORY_TAG_UNKNOWN. Re-class this allocation.");
    }

    // 📊 步骤 2: 更新统计
    stats.total_allocated += size;
    stats.tagged_allocations[tag] += size;

    // 💾 步骤 3: 分配内存
    void* block = platform_allocate(size, FALSE);

    // 🧹 步骤 4: 清零内存(重要!)
    platform_zero_memory(block, size);

    return block;
}

流程图

kallocate 被调用
tag == UNKNOWN?
输出警告
更新统计
调用 platform_allocate
清零内存
返回内存块

为什么自动清零?

// ❌ malloc 不会清零
int* arr = malloc(10 * sizeof(int));
// arr[0] 可能是 0xCDCDCDCD(随机数据)

// ✅ kallocate 自动清零
int* arr = kallocate(10 * sizeof(int), MEMORY_TAG_ARRAY);
// arr[0] 保证是 0

5️⃣ kfree() 实现

void kfree(void* block, u64 size, memory_tag tag) {
    // ⚠️ 步骤 1: 检查标签
    if (tag == MEMORY_TAG_UNKNOWN) {
        KWARN("kfree called using MEMORY_TAG_UNKNOWN. Re-class this allocation.");
    }

    // 📊 步骤 2: 更新统计(减法)
    stats.total_allocated -= size;
    stats.tagged_allocations[tag] -= size;

    // 💾 步骤 3: 释放内存
    platform_free(block, FALSE);
}

⚠️ 重要注意事项

// ❌ 错误:size 和 tag 不匹配
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 512, MEMORY_TAG_GAME);  // 统计错误!

// ❌ 错误:tag 不匹配
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 1024, MEMORY_TAG_TEXTURE);  // 统计错误!

// ✅ 正确
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 1024, MEMORY_TAG_GAME);

6️⃣ get_memory_usage_str() 实现

char* get_memory_usage_str() {
    // 定义单位
    const u64 gib = 1024 * 1024 * 1024;
    const u64 mib = 1024 * 1024;
    const u64 kib = 1024;

    // 8KB 缓冲区(足够大)
    char buffer[8000] = "System memory use (tagged):\n";
    u64 offset = strlen(buffer);

    // 遍历所有标签
    for (u32 i = 0; i < MEMORY_TAG_MAX_TAGS; ++i) {
        char unit[4] = "XiB";
        float amount = 1.0f;

        // 选择合适的单位
        if (stats.tagged_allocations[i] >= gib) {
            unit[0] = 'G';
            amount = stats.tagged_allocations[i] / (float)gib;
        } else if (stats.tagged_allocations[i] >= mib) {
            unit[0] = 'M';
            amount = stats.tagged_allocations[i] / (float)mib;
        } else if (stats.tagged_allocations[i] >= kib) {
            unit[0] = 'K';
            amount = stats.tagged_allocations[i] / (float)kib;
        } else {
            unit[0] = 'B';
            unit[1] = 0;  // "B" 而不是 "BiB"
            amount = (float)stats.tagged_allocations[i];
        }

        // 格式化输出
        i32 length = snprintf(buffer + offset, 8000,
                              "  %s: %.2f%s\n",
                              memory_tag_strings[i], amount, unit);
        offset += length;
    }

    // ⚠️ 使用 _strdup 复制字符串(需要调用者 free)
    char* out_string = _strdup(buffer);
    return out_string;
}

单位自动选择

范围单位示例
< 1 KiBB (字节)512.00B
1 KiB ~ 1 MiBKiB (千字节)2.50KiB
1 MiB ~ 1 GiBMiB (兆字节)16.75MiB
>= 1 GiBGiB (吉字节)2.30GiB

⚠️ 内存泄漏风险

char* usage = get_memory_usage_str();
KINFO(usage);
// ❌ 忘记释放!
// free(usage);  // 应该调用这个

改进建议(未来)

// ✅ 使用调用者提供的缓冲区
void get_memory_usage_str(char* buffer, u64 buffer_size);

// 或者使用线程局部存储
const char* get_memory_usage_str();  // 返回静态缓冲区

🔗 集成到引擎

📄 entry.h 更新
#include "core/kmemory.h"  // ← 新增

int main(void) {
    initialize_memory();  // ← 新增:初始化内存子系统

    // 创建游戏...
    game game_inst;
    if (!create_game(&game_inst)) {
        KFATAL("Could not create game!");
        return -1;
    }

    // 应用创建和运行...
    if (!application_create(&game_inst)) {
        KINFO("Application failed to create!.");
        return 1;
    }

    if(!application_run()) {
        KINFO("Application did not shutdown gracefully.");
        return 2;
    }

    shutdown_memory();  // ← 新增:关闭内存子系统

    return 0;
}

调用顺序

1. initialize_memory()     ← 最先调用
2. create_game()
3. application_create()
4. application_run()
5. shutdown_memory()        ← 最后调用
📄 application.c 更新
#include "core/kmemory.h"  // ← 新增

b8 application_run() {
    // ← 新增:输出内存使用报告
    KINFO(get_memory_usage_str());

    while (app_state.is_running) {
        // 游戏循环...
    }

    return TRUE;
}

输出示例

[INFO]: System memory use (tagged):
  UNKNOWN    : 0.00B
  ARRAY      : 0.00B
  DARRAY     : 0.00B
  ...
  GAME       : 48.00B
  ...
📄 testbed/src/entry.c 更新
// #include <platform/platform.h>  ← 删除
#include <core/kmemory.h>  // ← 新增

b8 create_game(game* out_game) {
    // ...

    // 创建游戏状态
    // out_game->state = platform_allocate(sizeof(game_state), FALSE);  ← 旧代码
    out_game->state = kallocate(sizeof(game_state), MEMORY_TAG_GAME);  // ← 新代码

    return TRUE;
}

改进点

方面旧代码新代码
追踪❌ 无法追踪✅ 标记为 GAME
统计❌ 不计入统计✅ 计入 GAME 类别
清零❌ 需要手动清零✅ 自动清零
📄 platform/platform.h 更新
// 移除 KAPI 导出标记
void* platform_allocate(u64 size, b8 aligned);  // ← 移除 KAPI
void platform_free(void* block, b8 aligned);    // ← 移除 KAPI

为什么移除 KAPI

// ❌ 之前:直接暴露给游戏
KAPI void* platform_allocate(...);  // 游戏可以直接调用

// ✅ 现在:只通过 kmemory 间接调用
void* platform_allocate(...);  // 私有 API
KAPI void* kallocate(...);     // 公共 API

层次关系

游戏代码
  ↓ 调用 kallocate (公共 API)
引擎 kmemory
  ↓ 调用 platform_allocate (私有 API)
平台层

📊 内存追踪详解

🔍 内存追踪原理

// 示例:分配和释放流程

// 1️⃣ 分配纹理内存
void* tex_data = kallocate(1024 * 1024 * 4, MEMORY_TAG_TEXTURE);
// stats.tagged_allocations[TEXTURE] = 4194304 (4 MiB)
// stats.total_allocated = 4194304

// 2️⃣ 分配游戏数据
void* game_data = kallocate(256, MEMORY_TAG_GAME);
// stats.tagged_allocations[TEXTURE] = 4194304
// stats.tagged_allocations[GAME] = 256
// stats.total_allocated = 4194560

// 3️⃣ 释放纹理
kfree(tex_data, 1024 * 1024 * 4, MEMORY_TAG_TEXTURE);
// stats.tagged_allocations[TEXTURE] = 0
// stats.tagged_allocations[GAME] = 256
// stats.total_allocated = 256

// 4️⃣ 查看报告
KINFO(get_memory_usage_str());

输出

[INFO]: System memory use (tagged):
  UNKNOWN    : 0.00B
  ARRAY      : 0.00B
  ...
  TEXTURE    : 0.00B
  GAME       : 256.00B
  ...

📈 内存使用可视化

假设我们有以下分配:

kallocate(100, MEMORY_TAG_ARRAY);
kallocate(2048, MEMORY_TAG_DARRAY);
kallocate(1024 * 1024, MEMORY_TAG_TEXTURE);
kallocate(512, MEMORY_TAG_GAME);
kallocate(4096, MEMORY_TAG_RENDERER);

内存分布图

╔════════════════════════════════════════════════════════╗
║  内存分配分布(总计:1.06 MiB)                         ║
╠════════════════════════════════════════════════════════╣
║  TEXTURE    ████████████████████████████  1.00 MiB    ║
║  RENDERER   █                              4.00 KiB   ║
║  DARRAY     █                              2.00 KiB   ║
║  GAME       ▌                              512.00 B   ║
║  ARRAY      ▌                              100.00 B   ║
╚════════════════════════════════════════════════════════╝

🐛 内存泄漏检测

示例场景

// testbed/src/game.c

b8 game_initialize(game* game_inst) {
    // 分配临时缓冲区
    void* temp_buffer = kallocate(1024, MEMORY_TAG_GAME);

    // ... 使用 temp_buffer ...

    // ❌ 忘记释放!
    // kfree(temp_buffer, 1024, MEMORY_TAG_GAME);

    return TRUE;
}

检测方法

  1. 启动时记录
initialize_memory();
KINFO(get_memory_usage_str());  // 全部应该是 0
  1. 运行中定期检查
// 在游戏循环中
static u32 frame_count = 0;
if (++frame_count % 3600 == 0) {  // 每 60 秒(假设 60 FPS)
    KINFO(get_memory_usage_str());
}
  1. 关闭前检查
shutdown_memory();
KINFO(get_memory_usage_str());  // 如果不是全 0,则有泄漏

泄漏检测输出

[INFO]: System memory use (tagged):
  UNKNOWN    : 0.00B
  ...
  GAME       : 1.00KiB  ← 泄漏!应该是 0
  ...

改进的 shutdown_memory()(未来版本):

void shutdown_memory() {
    // 检查是否有未释放的内存
    if (stats.total_allocated != 0) {
        KERROR("Memory leak detected! %llu bytes not freed.", stats.total_allocated);

        // 输出详细报告
        for (u32 i = 0; i < MEMORY_TAG_MAX_TAGS; ++i) {
            if (stats.tagged_allocations[i] != 0) {
                KWARN("  %s: %llu bytes",
                      memory_tag_strings[i],
                      stats.tagged_allocations[i]);
            }
        }
    } else {
        KINFO("All memory freed successfully!");
    }
}

🚀 使用示例

📝 基本使用

#include <core/kmemory.h>

// 示例:分配和使用内存

// 1. 分配数组
int* numbers = kallocate(10 * sizeof(int), MEMORY_TAG_ARRAY);

// 2. 使用(已自动清零)
for (int i = 0; i < 10; i++) {
    KASSERT_DEBUG(numbers[i] == 0);  // 保证清零
    numbers[i] = i * i;
}

// 3. 释放
kfree(numbers, 10 * sizeof(int), MEMORY_TAG_ARRAY);

🎮 游戏实体管理

// 示例:管理游戏实体

typedef struct entity {
    vec3 position;
    vec3 rotation;
    u32 id;
} entity;

// 创建实体
entity* create_entity(u32 id) {
    entity* e = kallocate(sizeof(entity), MEMORY_TAG_ENTITY);
    e->id = id;
    // position 和 rotation 已自动清零
    return e;
}

// 销毁实体
void destroy_entity(entity* e) {
    kfree(e, sizeof(entity), MEMORY_TAG_ENTITY);
}

// 使用
entity* player = create_entity(1);
entity* enemy = create_entity(2);

// 游戏逻辑...

destroy_entity(player);
destroy_entity(enemy);

🖼️ 纹理加载

// 示例:加载纹理

typedef struct texture {
    u32 width;
    u32 height;
    u8* pixels;
} texture;

texture* load_texture(const char* filepath) {
    texture* tex = kallocate(sizeof(texture), MEMORY_TAG_TEXTURE);

    // 假设从文件读取宽高
    tex->width = 1024;
    tex->height = 1024;

    // 分配像素数据(RGBA,每像素 4 字节)
    u64 pixel_size = tex->width * tex->height * 4;
    tex->pixels = kallocate(pixel_size, MEMORY_TAG_TEXTURE);

    // 加载像素数据...
    // load_pixels_from_file(filepath, tex->pixels);

    return tex;
}

void unload_texture(texture* tex) {
    u64 pixel_size = tex->width * tex->height * 4;
    kfree(tex->pixels, pixel_size, MEMORY_TAG_TEXTURE);
    kfree(tex, sizeof(texture), MEMORY_TAG_TEXTURE);
}

// 使用
texture* wall_tex = load_texture("wall.png");
// ... 使用纹理 ...
unload_texture(wall_tex);

📊 动态数组

// 示例:简单的动态数组

typedef struct darray {
    void* data;
    u64 capacity;
    u64 count;
    u64 element_size;
} darray;

darray* darray_create(u64 capacity, u64 element_size) {
    darray* arr = kallocate(sizeof(darray), MEMORY_TAG_DARRAY);

    arr->capacity = capacity;
    arr->count = 0;
    arr->element_size = element_size;
    arr->data = kallocate(capacity * element_size, MEMORY_TAG_DARRAY);

    return arr;
}

void darray_destroy(darray* arr) {
    kfree(arr->data, arr->capacity * arr->element_size, MEMORY_TAG_DARRAY);
    kfree(arr, sizeof(darray), MEMORY_TAG_DARRAY);
}

// 使用
darray* entities = darray_create(100, sizeof(entity));
// ... 使用数组 ...
darray_destroy(entities);

🔬 深入探讨

💡 内存对齐(未实现)

当前代码中有 TODO 注释:

// TODO: Memory alignment
void* block = platform_allocate(size, FALSE);

什么是内存对齐?

struct unaligned {
    char a;     // 1 字节
    int b;      // 4 字节
    char c;     // 1 字节
};
// 实际大小可能是 12 字节(有填充)

struct aligned {
    int b;      // 4 字节
    char a;     // 1 字节
    char c;     // 1 字节
};
// 实际大小可能是 8 字节

为什么需要对齐?

原因说明
性能CPU 访问对齐的内存更快
SIMDSSE/AVX 指令要求 16/32 字节对齐
原子操作某些原子操作要求对齐
硬件要求某些平台不支持未对齐访问

未来实现

// 对齐到 N 字节边界
void* kallocate_aligned(u64 size, u64 alignment, memory_tag tag);

// 示例:对齐到 16 字节(SSE)
float* vectors = kallocate_aligned(1000 * sizeof(float), 16, MEMORY_TAG_ARRAY);

🧩 内存池(未实现)

什么是内存池?

╔════════════════════════════════════════════════════════╗
║  内存池(Memory Pool)                                  ║
╠════════════════════════════════════════════════════════╣
║  预先分配一大块内存,分割成固定大小的块                 ║
║                                                        ║
║  ┌─────────────────────────────────────────────┐      ║
║  │ Pool (1MB)                                 │      ║
║  ├─────┬─────┬─────┬─────┬─────┬─────┬─────┬──┤      ║
║  │ 64B │ 64B │ 64B │ 64B │ 64B │ 64B │ 64B │  │      ║
║  │ Used│ Used│ Free│ Free│ Used│ Free│ Used│  │      ║
║  └─────┴─────┴─────┴─────┴─────┴─────┴─────┴──┘      ║
║                                                        ║
║  优势:                                                 ║
║  ✅ 快速分配/释放(O(1))                               ║
║  ✅ 减少内存碎片                                        ║
║  ✅ 缓存友好                                            ║
╚════════════════════════════════════════════════════════╝

适用场景

  • 频繁创建/销毁的对象(实体、粒子)
  • 固定大小的对象
  • 需要高性能的系统

未来实现

typedef struct memory_pool memory_pool;

memory_pool* pool_create(u64 block_size, u64 block_count);
void* pool_allocate(memory_pool* pool);
void pool_free(memory_pool* pool, void* block);
void pool_destroy(memory_pool* pool);

// 示例:粒子系统
memory_pool* particle_pool = pool_create(sizeof(particle), 10000);

for (int i = 0; i < 100; i++) {
    particle* p = pool_allocate(particle_pool);
    // ... 初始化粒子 ...
}

🔧 自定义分配器

分配器接口

typedef struct allocator {
    void* (*allocate)(struct allocator* self, u64 size);
    void (*free)(struct allocator* self, void* block, u64 size);
    void* state;
} allocator;

// 线性分配器(只增长,批量释放)
allocator* linear_allocator_create(u64 size);

// 栈分配器(后进先出)
allocator* stack_allocator_create(u64 size);

// 使用
allocator* alloc = linear_allocator_create(1024 * 1024);
void* block = alloc->allocate(alloc, 256);

⚡ 性能考虑

📊 性能分析

内存操作的开销

操作开销说明
kallocate~50-200ns取决于平台分配器
kfree~50-200ns取决于平台分配器
kzero_memory~0.5ns/byte非常快(memset 优化)
kcopy_memory~0.5ns/byte非常快(memcpy 优化)
统计更新~1ns简单的加减法

与标准库对比

// 基准测试(1000 次分配/释放)

// 使用 malloc/free
for (int i = 0; i < 1000; i++) {
    void* p = malloc(256);
    free(p);
}
// 时间:~150 μs

// 使用 kallocate/kfree
for (int i = 0; i < 1000; i++) {
    void* p = kallocate(256, MEMORY_TAG_ARRAY);
    kfree(p, 256, MEMORY_TAG_ARRAY);
}
// 时间:~155 μs(额外 3% 开销)

额外开销来源

  1. 统计更新stats.total_allocated += size;(~1ns)
  2. 标签检查if (tag == UNKNOWN)(~1ns)
  3. 自动清零platform_zero_memory(block, size)(~128ns for 256字节)

💡 优化建议

1. 减少小分配

// ❌ 低效:多次小分配
for (int i = 0; i < 100; i++) {
    int* val = kallocate(sizeof(int), MEMORY_TAG_ARRAY);
    // ...
}
// 100 次系统调用

// ✅ 高效:一次大分配
int* values = kallocate(100 * sizeof(int), MEMORY_TAG_ARRAY);
// 1 次系统调用

2. 复用内存

// ❌ 低效:每帧分配
void update() {
    void* temp = kallocate(1024, MEMORY_TAG_UNKNOWN);
    // ... 使用 temp ...
    kfree(temp, 1024, MEMORY_TAG_UNKNOWN);
}

// ✅ 高效:预先分配
static void* temp = NULL;

void initialize() {
    temp = kallocate(1024, MEMORY_TAG_APPLICATION);
}

void update() {
    kzero_memory(temp, 1024);  // 只清零,不重新分配
    // ... 使用 temp ...
}

3. 使用栈分配

// ✅ 栈分配(自动管理)
void process_data() {
    char buffer[1024];  // 栈上,无需 kallocate
    // ... 使用 buffer ...
}  // 自动释放

💡 最佳实践

✅ 推荐做法

实践说明示例
使用正确的标签始终使用合适的 memory_tagkallocate(size, MEMORY_TAG_TEXTURE)
成对使用kallocate 和 kfree 必须成对使用 RAII 模式(C++ 中)
记录大小在结构体中保存分配大小struct { void* data; u64 size; }
避免 UNKNOWN不要使用 MEMORY_TAG_UNKNOWN创建新标签代替
检查 NULL分配后检查返回值if (!ptr) KFATAL(...)

示例:结构体包含大小

typedef struct texture {
    u8* pixels;
    u64 pixel_size;  // ← 保存大小
    u32 width;
    u32 height;
} texture;

void unload_texture(texture* tex) {
    kfree(tex->pixels, tex->pixel_size, MEMORY_TAG_TEXTURE);  // ← 使用保存的大小
    kfree(tex, sizeof(texture), MEMORY_TAG_TEXTURE);
}

❌ 避免的错误

// ❌ 错误 1:忘记释放
void bad_function() {
    void* data = kallocate(1024, MEMORY_TAG_GAME);
    // ... 使用 data ...
    // 忘记 kfree!
}

// ✅ 正确
void good_function() {
    void* data = kallocate(1024, MEMORY_TAG_GAME);
    // ... 使用 data ...
    kfree(data, 1024, MEMORY_TAG_GAME);
}

// ❌ 错误 2:重复释放
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 1024, MEMORY_TAG_GAME);
kfree(data, 1024, MEMORY_TAG_GAME);  // ❌ 崩溃!

// ✅ 正确:置空指针
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 1024, MEMORY_TAG_GAME);
data = NULL;  // 防止重复释放

// ❌ 错误 3:大小不匹配
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 512, MEMORY_TAG_GAME);  // ❌ 统计错误

// ❌ 错误 4:标签不匹配
void* data = kallocate(1024, MEMORY_TAG_GAME);
kfree(data, 1024, MEMORY_TAG_TEXTURE);  // ❌ 统计错误

🐛 常见问题

❌ 问题 1:内存泄漏

现象

[WARN]: Memory leak detected! 1024 bytes not freed.
  GAME: 1024 bytes

原因:忘记调用 kfree

解决方案

// 在所有退出路径都释放内存
void load_level() {
    void* level_data = kallocate(1024, MEMORY_TAG_GAME);

    if (some_error) {
        kfree(level_data, 1024, MEMORY_TAG_GAME);  // ← 不要忘记
        return;
    }

    // 正常流程
    kfree(level_data, 1024, MEMORY_TAG_GAME);
}
❌ 问题 2:统计不准确

现象

[INFO]: GAME: -512.00B  // 负数!

原因:size 或 tag 不匹配

解决方案

// ✅ 使用宏确保一致性
#define ALLOC_TYPE(type, tag) \
    (type*)kallocate(sizeof(type), tag)

#define FREE_TYPE(ptr, type, tag) \
    kfree(ptr, sizeof(type), tag)

// 使用
entity* e = ALLOC_TYPE(entity, MEMORY_TAG_ENTITY);
FREE_TYPE(e, entity, MEMORY_TAG_ENTITY);
❌ 问题 3:`get_memory_usage_str()` 泄漏

现象MEMORY_TAG_STRING 持续增长

原因_strdup 的结果未释放

解决方案

// ✅ 记得释放
char* usage = get_memory_usage_str();
KINFO(usage);
free(usage);  // ← 重要!

🎯 本章总结

🎓 你学到了什么

🔧 技术技能

✅ 设计内存追踪系统
✅ 实现内存标签机制
✅ 统计内存使用情况
✅ 封装平台内存 API
✅ 生成内存使用报告
✅ 检测内存泄漏

💡 核心概念

✅ Memory Tagging
✅ 内存分类管理
✅ 自动清零内存
✅ 内存统计追踪
✅ 平台抽象
✅ 调试友好设计

🎯 关键要点

╔════════════════════════════════════════════════════════╗
║  🔑 内存子系统的核心设计原则                            ║
╠════════════════════════════════════════════════════════╣
║  1️⃣ 分类管理                                          ║
║     使用标签区分不同类型的内存分配                      ║
║                                                        ║
║  2️⃣ 自动清零                                          ║
║     所有新分配的内存自动初始化为 0                     ║
║                                                        ║
║  3️⃣ 统计追踪                                          ║
║     实时跟踪各类别的内存使用情况                        ║
║                                                        ║
║  4️⃣ 平台抽象                                          ║
║     隐藏平台特定的内存管理细节                          ║
╚════════════════════════════════════════════════════════╝

🔄 架构演进

阶段 1: 平台抽象层 (T04)
  ↓
阶段 2: 应用层与游戏循环 (T05)
  ↓
阶段 3: 内存子系统 (T06)         ← 我们在这里
  ↓
阶段 4: 事件系统 (下一章)
  ↓
阶段 5: 输入系统

📝 练习题

🥉 初级练习

1. 添加内存使用日志

任务:在游戏循环中每 5 秒输出一次内存使用报告。

参考答案

// game.c
static f64 memory_log_timer = 0.0;

b8 game_update(game* game_inst, f32 delta_time) {
    memory_log_timer += delta_time;

    if (memory_log_timer >= 5.0) {
        char* usage = get_memory_usage_str();
        KINFO(usage);
        free(usage);

        memory_log_timer = 0.0;
    }

    return TRUE;
}
2. 实现内存使用警告

任务:当某个标签的内存超过阈值时输出警告。

提示

// 检查纹理内存是否超过 100 MB
if (stats.tagged_allocations[MEMORY_TAG_TEXTURE] > 100 * 1024 * 1024) {
    KWARN("Texture memory exceeds 100 MB!");
}

🥈 中级练习

3. 实现内存泄漏检测

任务:改进 shutdown_memory(),输出所有未释放的内存。

参考实现

void shutdown_memory() {
    if (stats.total_allocated != 0) {
        KERROR("Memory leak detected! %llu bytes not freed.", stats.total_allocated);

        for (u32 i = 0; i < MEMORY_TAG_MAX_TAGS; ++i) {
            if (stats.tagged_allocations[i] != 0) {
                KWARN("  %s: %llu bytes",
                      memory_tag_strings[i],
                      stats.tagged_allocations[i]);
            }
        }
    } else {
        KINFO("All memory freed successfully!");
    }
}
4. 添加内存峰值追踪

任务:记录每个标签的内存使用峰值。

设计思路

struct memory_stats {
    u64 total_allocated;
    u64 tagged_allocations[MEMORY_TAG_MAX_TAGS];
    u64 tagged_peak[MEMORY_TAG_MAX_TAGS];  // ← 新增
};

void* kallocate(u64 size, memory_tag tag) {
    // ...
    stats.tagged_allocations[tag] += size;

    // 更新峰值
    if (stats.tagged_allocations[tag] > stats.tagged_peak[tag]) {
        stats.tagged_peak[tag] = stats.tagged_allocations[tag];
    }
    // ...
}

🥇 高级练习

5. 实现内存池分配器

任务:为固定大小的对象实现内存池。

设计思路

typedef struct memory_pool {
    void* memory;
    u64 block_size;
    u64 block_count;
    u64* free_blocks;  // 栈,存储空闲块索引
    u64 free_count;
} memory_pool;

memory_pool* pool_create(u64 block_size, u64 block_count);
void* pool_allocate(memory_pool* pool);
void pool_free(memory_pool* pool, void* block);
void pool_destroy(memory_pool* pool);
6. 实现内存调试模式

任务:在 Debug 模式下记录每次分配的堆栈跟踪。

提示

#ifdef KDEBUG
typedef struct allocation_info {
    void* address;
    u64 size;
    memory_tag tag;
    const char* file;
    u32 line;
} allocation_info;

#define kallocate(size, tag) kallocate_debug(size, tag, __FILE__, __LINE__)
#endif

🔗 参考资料

📚 官方文档

资源链接说明
C 内存管理C Memory Managementmalloc/free 文档
Valgrindvalgrind.org内存泄漏检测工具

📖 推荐阅读

  • 📘 《游戏引擎架构》第 5 章 - 内存管理
  • 📙 《Effective C》 - 内存管理最佳实践
  • 📕 《Game Engine Gems 2》 - 内存分配器设计

🎉 恭喜你掌握了内存子系统!

现在你已经拥有了强大的内存追踪和管理能力,可以轻松诊断内存问题。

下一步


📅 最后更新:2025-11-19
✍️ 作者:上手实验室

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值