💾 构建可追踪、可调试的内存管理系统
关注公众号【上手实验室】,领取章节视频教程
📋 目录
🎯 本章概述
📚 你将学到
✅ 自定义内存管理的重要性
✅ 内存标签(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) ║
║ • 检测内存泄漏 ║
║ • 检测重复释放 ║
║ • 检测越界访问(未来扩展) ║
╚════════════════════════════════════════════════════════╝
🏗️ 架构设计
📊 整体架构图
🔄 内存分配流程
💻 核心代码分析
🏷️ 内存标签定义 (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);
| 参数 | 类型 | 说明 |
|---|---|---|
size | u64 | 分配/释放的字节数 |
tag | memory_tag | 内存标签(分类) |
block | void* | 要释放的内存块指针 |
为什么 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_memory | memset(block, 0, size) | 清零内存 |
kcopy_memory | memcpy(dest, src, size) | 复制内存 |
kset_memory | memset(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;
}
流程图:
为什么自动清零?
// ❌ 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 KiB | B (字节) | 512.00B |
1 KiB ~ 1 MiB | KiB (千字节) | 2.50KiB |
1 MiB ~ 1 GiB | MiB (兆字节) | 16.75MiB |
>= 1 GiB | GiB (吉字节) | 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;
}
检测方法:
- 启动时记录:
initialize_memory();
KINFO(get_memory_usage_str()); // 全部应该是 0
- 运行中定期检查:
// 在游戏循环中
static u32 frame_count = 0;
if (++frame_count % 3600 == 0) { // 每 60 秒(假设 60 FPS)
KINFO(get_memory_usage_str());
}
- 关闭前检查:
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 访问对齐的内存更快 |
| SIMD | SSE/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% 开销)
额外开销来源:
- 统计更新:
stats.total_allocated += size;(~1ns) - 标签检查:
if (tag == UNKNOWN)(~1ns) - 自动清零:
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_tag | kallocate(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 Management | malloc/free 文档 |
| Valgrind | valgrind.org | 内存泄漏检测工具 |
📖 推荐阅读
- 📘 《游戏引擎架构》第 5 章 - 内存管理
- 📙 《Effective C》 - 内存管理最佳实践
- 📕 《Game Engine Gems 2》 - 内存分配器设计
🎉 恭喜你掌握了内存子系统!
现在你已经拥有了强大的内存追踪和管理能力,可以轻松诊断内存问题。
下一步:
- 📖 教程 07:事件系统
- 🔙 返回教程目录
📅 最后更新:2025-11-19
✍️ 作者:上手实验室
4834

被折叠的 条评论
为什么被折叠?



