📚 FMemStack 深度解析 - 虚幻引擎的"便签纸"内存管理器
🎯 一句话理解 FMemStack
FMemStack 就像一叠便签纸 📝 —— 你可以快速撕下一张写东西,用完后把整叠便签一起扔掉,而不需要一张张回收。
1. 什么是 FMemStack?
🏠 日常生活类比
想象你在做一道复杂的数学题:
| 场景 |
普通内存分配 |
FMemStack |
| 类比 |
每算一步就找一张新纸,算完再一张张收拾 |
用便签本,撕一张接一张写,最后整本扔掉 |
| 分配 |
每次 new 都要找空闲内存 |
直接在栈顶"撕"一块 |
| 释放 |
每个对象单独 delete |
一次性 Pop() 全部释放 |
| 速度 |
🐢 较慢 |
🚀 极快 |
📊 架构示意图
┌─────────────────────────────────────────────────────────────┐
│ FMemStack 结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Chunk 1 (64KB) │ │
│ │ ┌──────┬──────┬──────┬─────────────────────────┐ │ │
│ │ │ Obj1 │ Obj2 │ Obj3 │ 未使用空间 │ │ │
│ │ └──────┴──────┴──────┴─────────────────────────┘ │ │
│ │ ↑ │ │
│ │ Top │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Chunk 2 (64KB) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 全部未使用 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2. 类关系总览
📊 类继承与组合关系图
┌─────────────────────────────────────────────────────────────────────────────┐
│ FMemStack 类关系架构图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ FPageAllocator │ ◄─────────────────────────────────────────────┐ │
│ │ (页面分配器) │ │ │
│ │ ┌─────────────┐│ 提供底层 │ │
│ │ │ 64KB 大页 ││ 内存页面 ┌──────────────────────┐ │ │
│ │ │ 1KB 小页 ││ ──────────────→ │ FMemStackBase │ │ │
│ │ └─────────────┘│ │ (内存栈基类) │ │ │
│ └─────────────────┘ │ ┌────────────────┐ │ │ │
│ │ │ Top (栈顶) │ │ │ │
│ │ │ End (块结束) │ │ │ │
│ │ │ TopChunk │──┼──┐ │ │
│ │ │ TopMark ───────┼──┼──┼───┤ │
│ │ │ NumMarks │ │ │ │ │
│ │ └────────────────┘ │ │ │ │
│ └──────────┬───────────┘ │ │ │
│ │ 继承 │ │ │
│ ▼ │ │ │
│ ┌──────────────────────┐ │ │ │
│ │ FMemStack │ │ │ │
│ │ (线程单例内存栈) │ │ │ │
│ │ + TThreadSingleton │ │ │ │
│ │ + 强制 Mark 检查 │ │ │ │
│ └──────────────────────┘ │ │ │
│ │ │ │
│ ┌─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ FTaggedMemory │ ◄─────链表连接────── │ FMemMark │ │
│ │ (内存块头部) │ │ (内存标记/书签) │ │
│ │ ┌────────────────┐ │ │ ┌────────────────┐ │ │
│ │ │ Next (下一块) │ │ │ │ Mem (栈引用) │ │ │
│ │ │ DataSize │ │ │ │ Top (保存位置) │ │ │
│ │ │ Data() 数据区 │ │ │ │ SavedChunk │ │ │
│ │ └────────────────┘ │ │ │ NextTopmostMark│──┼─┐ │
│ └──────────────────────┘ │ │ bPopped │ │ │ │
│ │ └────────────────┘ │ │ │
│ └──────────────────────┘ │ │
│ ▲ │ │
│ └─────────────┘ │
│ Mark 链表 │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ TMemStackAllocator<Alignment> │ │
│ │ (容器分配器适配器) │ │
│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ForElementType<T> │ │ │
│ │ │ ├─ Data (数据指针) │ │ │
│ │ │ ├─ ResizeAllocation() ──调用──→ FMemStack::Get().PushBytes() │ │ │
│ │ │ └─ 支持 TArray、TSet 等容器 │ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
📋 类职责对照表
| 类名 |
职责 |
生命周期 |
关键方法 |
| FPageAllocator |
底层页面池管理 |
全局单例 |
Alloc(), Free(), AllocSmall() |
| FMemStackBase |
线性分配核心逻辑 |
栈对象/成员 |
Alloc(), PushBytes(), AllocateNewChunk() |
| FMemStack |
线程本地单例封装 |
线程生命周期 |
Get() (获取当前线程实例) |
| FTaggedMemory |
内存块元数据 |
随 Chunk 分配/释放 |
Data() (获取数据区指针) |
| FMemMark |
作用域内存管理 |
RAII 作用域 |
Pop() (释放标记后内存) |
| TMemStackAllocator |
容器分配器适配 |
随容器生命周期 |
ResizeAllocation() |
🔗 调用关系流程
用户代码
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. FMemMark Mark(FMemStack::Get()) │
│ └─→ 记录当前 Top 位置,链入 Mark 链表 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. new(FMemStack::Get()) T[n] 或 Mem.Alloc(size, align) │
│ └─→ FMemStackBase::Alloc() │
│ └─→ 移动 Top 指针 (快速路径) │
│ └─→ 或 AllocateNewChunk() (慢速路径) │
│ └─→ FPageAllocator::Alloc() 获取新页面 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Mark 析构 / Mark.Pop() │
│ └─→ 恢复 Top 到保存位置 │
│ └─→ FreeChunks() 释放多余 Chunk │
│ └─→ FPageAllocator::Free() 归还页面 │
└─────────────────────────────────────────────────────────────┘
🎭 类比总结
| 类 |
现实类比 |
作用 |
| FPageAllocator |
📦 仓库 |
提供大小固定的储物箱 |
| FMemStackBase |
📝 便签本 |
管理便签纸的撕取和使用 |
| FMemStack |
👤 个人便签本 |
每个人(线程)有自己专属的便签本 |
| FTaggedMemory |
📄 便签纸 |
实际写字的纸张,带有编号 |
| FMemMark |
🔖 书签 |
标记位置,方便一次性撕掉后面的纸 |
| TMemStackAllocator |
🗂️ 文件夹适配器 |
让文件夹(TArray)也能用便签纸 |
3. 核心概念详解
3.1 🧱 线性分配(Linear Allocation)
FMemStack 使用线性分配策略,这是最简单高效的内存分配方式:
cpp
FORCEINLINE void* Alloc(size_t AllocSize, size_t Alignment)
{
checkSlow(AllocSize >= 0);
checkSlow((Alignment & (Alignment - 1)) == 0);
checkSlow(Top <= End);
uint8* Result = Align(Top, Alignment);
uint8* NewTop = Result + AllocSize;
if (NewTop <= End)
{
Top = NewTop;
}
else
{
AllocateNewChunk(AllocSize + Alignment);
Result = Align(Top, Alignment);
Top = Result + AllocSize;
}
return Result;
}
📝 类比解释
普通 malloc/new:
┌────────────────────────────────────────┐
│ 1. 遍历空闲链表找合适大小的块 │
│ 2. 可能需要分割大块 │
│ 3. 更新各种元数据 │
│ 4. 返回指针 │
└────────────────────────────────────────┘
⏱️ 时间复杂度: O(n)
FMemStack::Alloc:
┌────────────────────────────────────────┐
│ 1. Top 指针 + AllocSize │
│ 2. 返回! │
└────────────────────────────────────────┘
⏱️ 时间复杂度: O(1)
3.2 🏷️ 标记系统(Mark System)
FMemStack 的精髓在于 FMemMark —— 它就像在便签本上做的"书签":
时间线:
─────────────────────────────────────────────────────────→
Mark1 Mark2 Pop(Mark2) Pop(Mark1)
↓ ↓ ↓ ↓
┌──────────────────────────────────────────────────────────────────┐
│ [Obj1][Obj2] [Obj3][Obj4][Obj5] │
│ ↑ ↑ │
│ Mark1位置 Mark2位置 │
└──────────────────────────────────────────────────────────────────┘
Pop(Mark2) 后:
┌──────────────────────────────────────────────────────────────────┐
│ [Obj1][Obj2] [ 空闲空间 ] │
└──────────────────────────────────────────────────────────────────┘
Pop(Mark1) 后:
┌──────────────────────────────────────────────────────────────────┐
│ [ 全部空闲空间 ] │
└──────────────────────────────────────────────────────────────────┘
4. 关键组件剖析
4.1 📦 FPageAllocator - 页面分配器
cpp
class FPageAllocator
{
public:
enum
{
PageSize = 64 * 1024,
SmallPageSize = 1024 - 16
};
void* Alloc();
void Free(void* Mem);
void* AllocSmall();
void FreeSmall(void* Mem);
};
🎭 类比:仓库管理员
| FPageAllocator |
仓库管理员 |
PageSize = 64KB |
大号储物箱 |
SmallPageSize = 1KB |
小号储物箱 |
Alloc() |
"给我一个大箱子" |
Free() |
"这个箱子我用完了" |
| 无锁分配器 |
每个员工有自己的储物区 |
4.2 📚 FMemStackBase - 内存栈基类
cpp
class FMemStackBase
{
public:
enum class EPageSize : uint8
{
Small,
Large
};
private:
uint8* Top;
uint8* End;
FTaggedMemory* TopChunk;
FMemMark* TopMark;
int32 NumMarks;
EPageSize PageSize;
};
📊 内存布局图
FMemStackBase 内部结构:
┌────────────────────────────────────────────────────────────┐
│ TopChunk ──→ ┌─────────────────────────────────────────┐ │
│ │ FTaggedMemory Header │ │
│ │ ├─ Next: 指向下一个 Chunk │ │
│ │ └─ DataSize: 数据区大小 │ │
│ ├─────────────────────────────────────────┤ │
│ │ [已分配数据][已分配数据][已分配数据] │ │
│ │ ↑ │ │
│ │ Top │ │
│ │ [ 未使用空间 ] │ │
│ │ ↑ │ │
│ │ End │ │
│ └─────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
🧱 Chunk(内存块)详解
FMemStack 不是一整块连续内存,而是由多个 Chunk(内存块/页面) 链接而成的链表结构:
┌─────────────────────────────────────────────────────────────────────────────┐
│ FMemStack 的 Chunk 链表结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Chunk 0 (64KB) Chunk 1 (64KB) Chunk 2 (64KB) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ FTaggedMemory │ │ FTaggedMemory │ │ FTaggedMemory │ │
│ │ ├─ Next ────────┼──► │ ├─ Next ────────┼──► │ ├─ Next ──► null│ │
│ │ └─ DataSize │ │ └─ DataSize │ │ └─ DataSize │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ │ │ │ │ │ │
│ │ 已用数据 │ │ 已用数据 │ │ 部分使用 │ │
│ │ (已满) │ │ (已满) │ │ ↑Top │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ ↑ ↑ │
│ 第一个 Chunk TopChunk │
│ (可能已满) (当前正在使用的) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
🔍 为什么需要多个 Chunk?
当单个 Chunk 容量不足时,会自动申请新的 Chunk:
场景:分配超过当前 Chunk 剩余容量的数据
1. 初始状态 - 只有一个 Chunk
┌────────────────────────────────────────┐
│ Chunk 0 (64KB) │
│ [已用 60KB] │ Top ▼ │ 剩余 4KB │
└────────────────────────────────────────┘
2. 请求分配 10KB → 当前 Chunk 放不下!
3. 从 FPageAllocator 获取新 Chunk,在新 Chunk 中分配
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ Chunk 0 (64KB) - 已满 │ ──► │ Chunk 1 (64KB) - 新分配 │
│ [██████████████████████████████████] │ │ [10KB 新数据] │ Top ▼ │ 剩余 54KB │
└────────────────────────────────────────┘ └────────────────────────────────────────┘
↑
TopChunk 指向这里
📌 FMemStack 中的关键指针
cpp
class FMemStackBase
{
uint8* Top;
uint8* End;
FTaggedMemory* TopChunk;
};
┌─────────────────────────────────────────────────────────────────────────────┐
│ 指针关系详解 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ TopChunk 指向当前 Chunk │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ FTaggedMemory (Chunk 结构) │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ Next ────────────────────► 下一个 Chunk (或 nullptr) │ │
│ │ DataSize ────────────────► 64KB (或其他大小) │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ Data[DataSize] 实际存储区域 │ │
│ │ │ │
│ │ [已分配的数据............] │ Top ▼ │ [可用空间....] │ End ▼ │ │
│ │ │ │ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ ↑ ↑ │
│ Top End │
│ (下次分配位置) (Chunk 边界) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
🔄 FMemMark 为什么要记录 SavedChunk?
当分配跨越多个 Chunk 时,回滚需要知道回到哪个 Chunk:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 跨 Chunk 分配的回滚场景 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Mark 创建时 - 在 Chunk 0 │
│ ┌────────────────────────────┐ │
│ │ Chunk 0 │ FMemMark 记录: │
│ │ [数据] │📌Mark│ Top ▼ │ • SavedTop = Top 位置 │
│ └────────────────────────────┘ • SavedChunk = Chunk 0 │
│ │
│ 2. 分配大量数据,跨越到 Chunk 1 │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ Chunk 0 (已满) │ ──► │ Chunk 1 │ │
│ │ [数据] │📌Mark│ [新数据██] │ │ [新数据███] │ Top ▼ │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ ↑ │
│ TopChunk │
│ │
│ 3. Mark 析构 - 需要回滚到 Chunk 0! │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ Chunk 0 │ │ Chunk 1 (归还给页面池) │ │
│ │ [数据] │ Top ▼ (恢复) │ │ [释放] │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ ↑ │
│ TopChunk 也要恢复到 Chunk 0 │
│ │
│ ⚠️ 如果只记录 SavedTop 而不记录 SavedChunk,就无法正确回滚! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
📋 Chunk 概念总结
| 概念 |
说明 |
| Chunk |
一块固定大小的内存页面(通常 64KB),多个 Chunk 通过 Next 指针链接成链表 |
| FTaggedMemory |
Chunk 的数据结构,包含 Next 指针和 DataSize |
| TopChunk |
当前正在使用的 Chunk |
| Top |
TopChunk 内的当前分配位置 |
| End |
TopChunk 的边界(分配不能超过此位置) |
| SavedChunk |
FMemMark 记录的 Chunk,用于跨 Chunk 回滚 |
4.3 🔖 FMemMark - 内存标记
cpp
class FMemMark
{
public:
FMemMark(FMemStackBase& InMem)
: Mem(InMem)
, Top(InMem.Top)
, SavedChunk(InMem.TopChunk)
, bPopped(false)
, NextTopmostMark(InMem.TopMark)
{
Mem.TopMark = this;
Mem.NumMarks++;
}
void Pop()
{
if (!bPopped)
{
check(Mem.TopMark == this);
bPopped = true;
--Mem.NumMarks;
if (SavedChunk != Mem.TopChunk)
{
Mem.FreeChunks(SavedChunk);
}
Mem.Top = Top;
Mem.TopMark = NextTopmostMark;
}
}
private:
FMemStackBase& Mem;
uint8* Top;
FTaggedMemory* SavedChunk;
bool bPopped;
FMemMark* NextTopmostMark;
};
🔗 TopMark 链表更新机制
TopMark 是 FMemStackBase 中维护的链表头指针,指向最近创建的 FMemMark,多个 Mark 通过 NextTopmostMark 形成单向链表(栈结构)。
创建 Mark 时 - 链入链表头部
cpp
FMemMark(FMemStackBase& InMem)
: NextTopmostMark(InMem.TopMark)
{
Mem.TopMark = this;
Mem.NumMarks++;
}
Pop/析构 Mark 时 - 从链表头部移除
cpp
void Pop()
{
check(Mem.TopMark == this);
Mem.TopMark = NextTopmostMark;
}
📊 TopMark 链表更新流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ TopMark 链表更新过程 │
└─────────────────────────────────────────────────────────────────────────────┘
【初始状态】
┌─────────────────────┐
│ FMemStackBase │
│ TopMark ──► nullptr │
│ NumMarks = 0 │
└─────────────────────┘
【创建 Mark1】 FMemMark Mark1(Mem);
┌─────────────────────┐ ┌─────────────────────┐
│ FMemStackBase │ │ FMemMark (Mark1) │
│ TopMark ────────────┼──► │ NextTopmostMark ──► nullptr
│ NumMarks = 1 │ │ SavedTop = 位置A │
└─────────────────────┘ └─────────────────────┘
【创建 Mark2】 FMemMark Mark2(Mem);
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ FMemStackBase │ │ FMemMark (Mark2) │ │ FMemMark (Mark1) │
│ TopMark ────────────┼──► │ NextTopmostMark ────┼──► │ NextTopmostMark ──► nullptr
│ NumMarks = 2 │ │ SavedTop = 位置B │ │ SavedTop = 位置A │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
【创建 Mark3】 FMemMark Mark3(Mem);
┌─────────────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ FMemStackBase │ │ Mark3 │ │ Mark2 │ │ Mark1 │
│ TopMark ────────────┼──► │ Next ────┼──► │ Next ────┼──► │ Next ──► nullptr
│ NumMarks = 3 │ └──────────┘ └──────────┘ └──────────┘
└─────────────────────┘
【Mark3 析构/Pop】 ~FMemMark() 或 Mark3.Pop()
┌─────────────────────┐ ┌──────────┐ ┌──────────┐
│ FMemStackBase │ │ Mark2 │ │ Mark1 │
│ TopMark ────────────┼──► │ Next ────┼──► │ Next ──► nullptr
│ NumMarks = 2 │ └──────────┘ └──────────┘
└─────────────────────┘
TopMark 恢复为 Mark2
【Mark2 析构/Pop】
┌─────────────────────┐ ┌──────────┐
│ FMemStackBase │ │ Mark1 │
│ TopMark ────────────┼──► │ Next ──► nullptr
│ NumMarks = 1 │ └──────────┘
└─────────────────────┘
【Mark1 析构/Pop】
┌─────────────────────┐
│ FMemStackBase │
│ TopMark ──► nullptr │
│ NumMarks = 0 │
└─────────────────────┘
⚠️ 为什么必须按 LIFO 顺序 Pop?
┌─────────────────────────────────────────────────────────────────────────────┐
│ LIFO 约束 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 正确顺序(LIFO - 后进先出): │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ FMemMark Mark1(Mem);
│ │ { │ │
│ │ FMemMark Mark2(Mem);
│ │
│ │ }
│ │ }
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ❌ 错误顺序(会触发 check 失败): │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ FMemMark* Mark1 = new FMemMark(Mem); │ │
│ │ FMemMark* Mark2 = new FMemMark(Mem); │ │
│ │ Mark1->Pop();
│ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ 原因:Mark 链表是栈结构,只能从栈顶(最新的)开始 Pop │
│ 这也是为什么推荐在栈上创建 FMemMark(利用 C++ 析构顺序保证 LIFO) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
📋 TopMark 操作总结
| 操作 |
TopMark 变化 |
NumMarks 变化 |
| 创建 FMemMark |
TopMark = this (新 Mark 成为链表头) |
+1 |
| Pop/析构 |
TopMark = NextTopmostMark (恢复上一个) |
-1 |
🎭 类比:游戏存档点
┌─────────────────────────────────────────────────────────────┐
│ 游戏存档类比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 玩游戏时: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [开始] → [存档1] → 打怪 → [存档2] → Boss战 → 失败 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 读取存档2:回到 Boss 战之前 │
│ 读取存档1:回到打怪之前 │
│ │
│ FMemMark 工作原理完全相同! │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [初始] → [Mark1] → 分配 → [Mark2] → 分配 → Pop │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
4.4 🧮 TMemStackAllocator - 容器分配器
cpp
template<uint32 Alignment = DEFAULT_ALIGNMENT>
class TMemStackAllocator
{
public:
using SizeType = int32;
template<typename ElementType>
class ForElementType
{
public:
void ResizeAllocation(SizeType CurrentNum, SizeType NewMax, SIZE_T NumBytesPerElement)
{
void* OldData = Data;
if (NewMax)
{
Data = (ElementType*)FMemStack::Get().PushBytes(
(int32)(NewMax * NumBytesPerElement),
FMath::Max(Alignment, (uint32)alignof(ElementType))
);
if (OldData && CurrentNum)
{
const SizeType NumCopiedElements = FMath::Min(NewMax, CurrentNum);
FMemory::Memcpy(Data, OldData, NumCopiedElements * NumBytesPerElement);
}
}
}
private:
ElementType* Data;
};
};
5. 实际使用示例
5.1 🔰 基础用法
cpp
void ProcessGameFrame()
{
FMemMark Mark(FMemStack::Get());
FVector* TempPositions = new(FMemStack::Get()) FVector[1000];
FRotator* TempRotations = new(FMemStack::Get()) FRotator[1000];
for (int32 i = 0; i < 1000; i++)
{
TempPositions[i] = CalculatePosition(i);
TempRotations[i] = CalculateRotation(i);
}
ApplyTransforms(TempPositions, TempRotations, 1000);
}
🔍 代码逐行深度解析
第1步:获取线程本地的 FMemStack 并创建标记
cpp
FMemMark Mark(FMemStack::Get());
| 操作 |
说明 |
FMemStack::Get() |
获取当前线程专属的 FMemStack 实例(线程单例) |
FMemMark Mark(...) |
在栈上创建标记,记录当前 Top 位置 |
创建 Mark 后的 FMemStack 状态:
┌────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [ 可能有之前的数据 ] [ 空闲空间 ] │ │
│ │ ↑ │ │
│ │ Top (Mark 记录此位置) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
第2步:使用 placement new 从 FMemStack 分配数组
cpp
FVector* TempPositions = new(FMemStack::Get()) FVector[1000];
FRotator* TempRotations = new(FMemStack::Get()) FRotator[1000];
这里使用了 C++ 的 placement new 数组版本,让我们深入理解其工作原理:
🔍 Placement New 语法解析
new(FMemStack::Get()) FVector[1000]
│ │ │ │
│ │ │ └── 数组大小,编译器算入 Size 参数
│ │ └── 元素类型
│ └── placement 参数,匹配 FMemStackBase&
└── 因为有 [],调用 operator new[](不是 operator new)
🎯 C++ 重载匹配过程
当编译器看到 new(FMemStack::Get()) FVector[1000] 时:
- 识别为数组分配 → 因为有
[1000],查找 operator new[]
- 匹配 placement 参数 →
FMemStack::Get() 返回 FMemStack&,继承自 FMemStackBase
- 找到匹配的重载:
cpp
inline void* operator new[](size_t Size, FMemStackBase& Mem, int32 Count = 1)
{
const size_t SizeInBytes = Size * Count;
checkSlow(SizeInBytes <= (size_t)TNumericLimits<int32>::Max());
return Mem.PushBytes(SizeInBytes, __STDCPP_DEFAULT_NEW_ALIGNMENT__);
}
📊 参数传递流程
new(FMemStack::Get()) FVector[1000]
│ │
│ └──→ 编译器计算: Size = sizeof(FVector) * 1000 = 12000
│
└──→ 作为第二个参数传给 operator new[]
最终调用: operator new[](12000, FMemStack::Get(), 1)
↑ ↑ ↑
Size Mem Count(默认值)
| 参数 |
值 |
来源 |
Size |
12000 |
编译器计算 sizeof(FVector) * 1000 |
Mem |
FMemStack 引用 |
placement 参数 |
Count |
1 |
默认参数(此场景未使用) |
🤔 Count 参数的设计意图
Count 参数在数组语法中实际上是冗余的(因为 Size 已包含数组大小),它是为另一种用法设计的:
cpp
FVector* arr = new(Mem) FVector[1000];
FVector* arr = new(Mem, 1000) FVector;
📋 MemStack.h 中所有 operator new 重载
cpp
void* operator new(size_t Size, FMemStackBase& Mem, int32 Count = 1);
void* operator new(size_t Size, std::align_val_t Align, FMemStackBase& Mem, int32 Count = 1);
void* operator new[](size_t Size, FMemStackBase& Mem, int32 Count = 1);
void* operator new[](size_t Size, std::align_val_t Align, FMemStackBase& Mem, int32 Count = 1);
void* operator new(size_t Size, FMemStackBase& Mem, EMemZeroed Tag, int32 Count = 1);
void* operator new[](size_t Size, FMemStackBase& Mem, EMemZeroed Tag, int32 Count = 1);
void* operator new(size_t Size, FMemStackBase& Mem, EMemOned Tag, int32 Count = 1);
void* operator new[](size_t Size, FMemStackBase& Mem, EMemOned Tag, int32 Count = 1);
| 分配 |
大小计算 |
实际字节数 |
FVector[1000] |
sizeof(FVector) * 1000 |
12 * 1000 = 12000 字节 |
FRotator[1000] |
sizeof(FRotator) * 1000 |
12 * 1000 = 12000 字节 |
分配后的 FMemStack 状态:
┌────────────────────────────────────────────────────────────┐
│ Mark位置 │
│ ↓ │
│ ┌──┬─────────────────┬─────────────────┬───────────────┐ │
│ │旧│ TempPositions │ TempRotations │ 空闲空间 │ │
│ │数│ FVector[1000] │ FRotator[1000] │ │ │
│ │据│ 12000 bytes │ 12000 bytes │ │ │
│ └──┴─────────────────┴─────────────────┴───────────────┘ │
│ ↑ │
│ Top │
└────────────────────────────────────────────────────────────┘
第3步:正常使用分配的内存
cpp
for (int32 i = 0; i < 1000; i++)
{
TempPositions[i] = CalculatePosition(i);
TempRotations[i] = CalculateRotation(i);
}
ApplyTransforms(TempPositions, TempRotations, 1000);
使用方式与普通 new 分配的数组完全相同,没有任何区别。
第4步:函数结束,Mark 自动析构
┌─────────────────────────────────────────────────────────────┐
│ 函数结束,局部变量按逆序析构 │
│ │
│ 1. Mark.~FMemMark() 被调用 │
│ ↓ │
│ 2. 内部调用 Pop() │
│ ↓ │
│ 3. Top 指针恢复到 Mark 创建时的位置 │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [旧数据] [ 全部变成可用空间 ] │ │
│ │ ↑ │ │
│ │ Top (恢复到 Mark 位置) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ✨ 24000 字节瞬间"释放",无需遍历,无需调用 free │
└─────────────────────────────────────────────────────────────┘
🗺️ 内存结构演变全景图
下面展示整个函数执行过程中 FMemStack 内存的完整变化:
┌─────────────────────────────────────────────────────────────────────────────┐
│ FMemStack 内存结构演变 │
└─────────────────────────────────────────────────────────────────────────────┘
【阶段 1】初始状态 - 函数入口
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FMemStack::Get() 返回线程本地栈:
┌──────────────────────────────────────────────────────────────┐
│ FMemStack (线程本地) │
├──────────────────────────────────────────────────────────────┤
│ Top ──────────────────────────────► [某个位置] │
│ End ──────────────────────────────► [页面末尾] │
│ TopChunk ─────────────────────────► [当前页面] │
└──────────────────────────────────────────────────────────────┘
内存页面 (假设 64KB):
┌────────────────────────────────────────────────────────────────────┐
│ [已使用的内存...] │ Top ▼ │ 可用空间... │
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │
└────────────────────────────────────────────────────────────────────┘
💡 为什么入口时可能有"已使用的内存"?
FMemStack 是线程级复用的,不是每个函数新建的!
┌─────────────────────────────────────────────────────────────────────────────┐
│ FMemStack 的生命周期 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 线程启动 │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ FMemStack::Get() 创建线程本地的 FMemStack(整个线程只有一个实例) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ├──► 函数 A 调用 ──► FMemMark ──► 分配 ──► Mark 析构释放 │
│ │ │
│ ├──► 函数 B 调用 ──► FMemMark ──► 分配 ──► Mark 析构释放 │
│ │ │
│ ├──► 函数 C 调用 ──► 嵌套调用 ProcessGameFrame() ◄── 我们的例子 │
│ │ │ │
│ │ └──► 此时 FMemStack 中可能已有函数 C 的数据! │
│ │ │
│ ▼ │
│ 线程结束,FMemStack 销毁 │
└─────────────────────────────────────────────────────────────────────────────┘
嵌套调用场景示例:
void OuterFunction()
{
FMemMark OuterMark(FMemStack::Get());
FSomeData* OuterData = new(FMemStack::Get()) FSomeData[100];
ProcessGameFrame();
}
| 情况 | 入口时状态 |
|------|-----------|
| 第一次使用 FMemStack | 空的,Top 在起始位置 |
| 有外层 FMemMark 未释放 | 有已用内存,Top 在中间某位置 |
| 上一个 FMemMark 已释放 | 空的(或回到之前状态) |
✨ FMemMark 的价值:无论之前状态如何,它只负责释放自己标记之后分配的内存!
【阶段 2】FMemMark Mark(FMemStack::Get()) 执行后
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Mark 记录当前位置:
┌──────────────────────────────────────────────────────────────┐
│ FMemMark (栈上对象) │
├──────────────────────────────────────────────────────────────┤
│ Mem ──────────────────────────────► FMemStack │
│ SavedTop ─────────────────────────► [记录的 Top 位置] 📌 │
│ SavedChunk ───────────────────────► [记录的 TopChunk] │
│ bPopped ──────────────────────────► false │
└──────────────────────────────────────────────────────────────┘
内存页面:
┌────────────────────────────────────────────────────────────────────┐
│ [已使用的内存...] │ 📌Mark │ Top ▼ │ 可用空间... │
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ SavedTop│░░░░░░░░░░░░░░░░░░░│ │
└────────────────────────────────────────────────────────────────────┘
↑
Mark 记录了这个位置!
【阶段 3】分配 FVector[1000] 后
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FVector 大小 = 12 字节 (3 × float)
FVector[1000] = 12,000 字节 ≈ 11.7 KB
new(FMemStack::Get()) FVector[1000] 执行:
1. PushBytes(12000, alignment) 被调用
2. Top 指针向前移动 12000 字节
3. 返回原 Top 位置作为数组起始地址
内存页面:
┌────────────────────────────────────────────────────────────────────┐
│ [已用] │📌Mark│ TempPositions[1000] │ Top ▼│ 可用空间 │
│▓▓▓▓▓▓▓▓│SavedTop│ FVector × 1000 (12KB) │░░░░░░│ │
│ │ │ [0][1][2]...[999] │ │ │
└────────────────────────────────────────────────────────────────────┘
├─────── 12,000 bytes ─────────┤
【阶段 4】分配 FRotator[1000] 后
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FRotator 大小 = 12 字节 (Pitch, Yaw, Roll 各 4 字节)
FRotator[1000] = 12,000 字节 ≈ 11.7 KB
总共分配: 24,000 字节 ≈ 23.4 KB
内存页面:
┌────────────────────────────────────────────────────────────────────────────┐
│[已用]│📌│ TempPositions[1000] │ TempRotations[1000] │Top▼│ 可用 │
│▓▓▓▓▓▓│ │ FVector × 1000 │ FRotator × 1000 │░░░░│ │
│ │ │ (12KB) │ (12KB) │ │ │
└────────────────────────────────────────────────────────────────────────────┘
├────── 12KB ──────────┼────── 12KB ─────────┤
│ │
└────────────── 总共 ~24KB ──────────────────┘
【阶段 5】函数结束,Mark 析构
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
~FMemMark() 执行:
1. 调用 Mem.PopTo(SavedTop, SavedChunk)
2. Top 指针回退到 SavedTop 位置
3. 24KB 内存瞬间"释放"(实际只是移动指针)
内存页面 (恢复到阶段 2):
┌────────────────────────────────────────────────────────────────────┐
│ [已使用的内存...] │ Top ▼ (回到 SavedTop) │ 可用空间... │
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │
└────────────────────────────────────────────────────────────────────┘
↑
Top 回到这里,之前的 24KB 被"覆盖释放"
📊 关键数据汇总
| 数据项 |
大小 |
计算方式 |
FVector |
12 字节 |
float X, Y, Z = 3 × 4 |
FRotator |
12 字节 |
float Pitch, Yaw, Roll = 3 × 4 |
TempPositions |
12,000 字节 |
12 × 1000 |
TempRotations |
12,000 字节 |
12 × 1000 |
| 总分配 |
24,000 字节 |
≈ 23.4 KB |
✨ 内存布局特点
┌─────────────────────────────────────────────────────────────┐
│ 线性连续分配 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 低地址 高地址 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────┬────────────────┬────────────────┬──────┐ │
│ │ 旧 │ TempPositions │ TempRotations │ 新 │ │
│ │ 数据 │ [0]...[999] │ [0]...[999] │ 分配 │ │
│ └──────┴────────────────┴────────────────┴──────┘ │
│ ↑ ↑ │
│ SavedTop Top │
│ │
│ ✅ 优点: │
│ • 内存连续,缓存友好 (Cache-Friendly) │
│ • 分配 O(1),只需移动指针 │
│ • 释放 O(1),只需回退指针 │
│ • 无内存碎片 │
│ │
└─────────────────────────────────────────────────────────────┘
⚡ 与传统 new/delete 对比
cpp
void ProcessGameFrame_Traditional()
{
FVector* TempPositions = new FVector[1000];
FRotator* TempRotations = new FRotator[1000];
delete[] TempPositions;
delete[] TempRotations;
}
void ProcessGameFrame_MemStack()
{
FMemMark Mark(FMemStack::Get());
FVector* TempPositions = new(FMemStack::Get()) FVector[1000];
FRotator* TempRotations = new(FMemStack::Get()) FRotator[1000];
}
| 对比项 |
传统 new/delete |
FMemStack |
| 分配速度 |
🐢 O(n) 查找空闲块 |
🚀 O(1) 移动指针 |
| 释放速度 |
🐢 每个 delete 单独处理 |
🚀 O(1) 恢复指针 |
| 内存碎片 |
❌ 会产生 |
✅ 不会产生 |
| 异常安全 |
⚠️ 需要 RAII 包装 |
✅ 天然异常安全 |
| 忘记释放 |
💥 内存泄漏 |
✅ 自动释放 |
5.2 🎨 不同分配方式
cpp
void DifferentAllocationStyles()
{
FMemMark Mark(FMemStack::Get());
FMemStack& Mem = FMemStack::Get();
int32* RawData = new(Mem) int32[100];
int32* ZeroData = new(Mem, MEM_Zeroed) int32[100];
int32* OneData = new(Mem, MEM_Oned) int32[100];
FVector* Vectors = New<FVector>(Mem, 50);
FVector* ZeroVectors = NewZeroed<FVector>(Mem, 50);
}
📊 分配方式对比表
| 分配方式 |
语法 |
初始化 |
适用场景 |
| 普通 |
new(Mem) T[n] |
❌ 无 |
立即赋值的数据 |
| 零初始化 |
new(Mem, MEM_Zeroed) T[n] |
✅ 全0 |
计数器、标志位 |
| 全1初始化 |
new(Mem, MEM_Oned) T[n] |
✅ 全0xFF |
特殊标记值 |
| 模板 |
New<T>(Mem, n) |
❌ 无 |
类型安全分配 |
| 模板零初始化 |
NewZeroed<T>(Mem, n) |
✅ 全0 |
类型安全+零初始化 |
5.3 🔄 嵌套 Mark 使用
cpp
void NestedMarksExample()
{
FMemStack& Mem = FMemStack::Get();
FMemMark OuterMark(Mem);
FMatrix* TransformCache = new(Mem) FMatrix[100];
for (int32 Frame = 0; Frame < 60; Frame++)
{
FMemMark FrameMark(Mem);
FVector* FramePositions = new(Mem) FVector[1000];
ProcessFrame(TransformCache, FramePositions);
}
}
📊 嵌套 Mark 内存变化图
时间 →
─────────────────────────────────────────────────────────────→
内存使用量
↑
│ ┌───┐ ┌───┐ ┌───┐
│ │ │ │ │ │ │ ← 每帧临时数据
│ ┌───┴───┴─┴───┴─┴───┴───────────┐
│ │ │ ← TransformCache
│ │ │
└─┴────────────────────────────────┴──→ 时间
↑ ↑
OuterMark OuterMark.Pop()
创建 (析构时)
5.4 🎮 实战:渲染系统中的使用
cpp
void FSceneRenderer::GatherVisiblePrimitives()
{
FMemMark Mark(FMemStack::Get());
FVisibilityData* VisData = new(FMemStack::Get())
FVisibilityData[Scene->Primitives.Num()];
ParallelFor(Scene->Primitives.Num(), [&](int32 Index)
{
VisData[Index].bVisible =
ViewFrustum.IntersectBox(Scene->Primitives[Index]->Bounds);
});
TArray<FPrimitiveSceneProxy*, TMemStackAllocator<>> VisibleProxies;
for (int32 i = 0; i < Scene->Primitives.Num(); i++)
{
if (VisData[i].bVisible)
{
VisibleProxies.Add(Scene->Primitives[i]);
}
}
RenderPrimitives(VisibleProxies);
}
5.5 📦 配合 TArray 使用
cpp
void UseWithTArray()
{
FMemMark Mark(FMemStack::Get());
TArray<int32, TMemStackAllocator<>> TempNumbers;
TArray<FVector, TMemStackAllocator<>> TempPositions;
TempNumbers.Reserve(1000);
for (int32 i = 0; i < 1000; i++)
{
TempNumbers.Add(i * 2);
TempPositions.Add(FVector(i, i, i));
}
TempNumbers.Sort();
int32* Found = TempNumbers.FindByPredicate([](int32 Val) { return Val > 500; });
}
🔍 代码逐行深度解析
第1步:创建内存标记(必须!)
cpp
FMemMark Mark(FMemStack::Get());
FMemMark 必须在分配任何内存之前创建,它记录了当前栈顶位置。当 Mark 析构时,会把栈顶恢复到这个位置,从而释放所有后续分配的内存。
第2步:声明使用 MemStack 分配器的 TArray
cpp
TArray<int32, TMemStackAllocator<>> TempNumbers;
TArray<FVector, TMemStackAllocator<>> TempPositions;
TArray 的第二个模板参数是分配器类型。默认是 FDefaultAllocator(使用 malloc/free),这里换成 TMemStackAllocator<> 后,TArray 的内存分配会从 FMemStack 获取。
📊 内存分配流程对比
普通 TArray<int32>: TArray<int32, TMemStackAllocator<>>:
┌─────────────────────┐ ┌─────────────────────────────────────┐
│ TempNumbers.Add() │ │ TempNumbers.Add() │
│ ↓ │ │ ↓ │
│ FDefaultAllocator │ │ TMemStackAllocator │
│ ↓ │ │ ↓ │
│ FMemory::Malloc() │ │ FMemStack::Get().PushBytes() │
│ ↓ │ │ ↓ │
│ 系统堆内存 │ │ FMemStack 线性内存 │
│ ↓ │ │ ↓ │
│ 需要手动/析构释放 │ │ FMemMark::Pop() 统一释放 │
└─────────────────────┘ └─────────────────────────────────────┘
第3步:预分配空间时内部发生了什么
cpp
TempNumbers.Reserve(1000);
内部调用 TMemStackAllocator::ResizeAllocation:
cpp
Data = (int32*)FMemStack::Get().PushBytes(
1000 * sizeof(int32),
alignof(int32)
);
📊 内存布局变化图
FMemStack 内存布局(添加元素后):
┌────────────────────────────────────────────────────────────┐
│ Mark位置 │
│ ↓ │
│ ┌──────────────────────┬──────────────────────┬─────────┐ │
│ │ TempNumbers 数据区 │ TempPositions 数据区 │ 空闲 │ │
│ │ [0,2,4,6,8...] │ [(0,0,0),(1,1,1)...] │ │ │
│ │ 4000 bytes │ 12000 bytes │ │ │
│ └──────────────────────┴──────────────────────┴─────────┘ │
│ ↑ │
│ Top │
└────────────────────────────────────────────────────────────┘
第4步:正常使用 TArray 功能
cpp
TempNumbers.Sort();
int32* Found = TempNumbers.FindByPredicate([](int32 Val) { return Val > 500; });
分配器只影响内存来源,不影响 TArray 的功能。排序、查找、迭代等所有操作都正常工作。
第5步:函数结束,Mark 析构
┌─────────────────────────────────────────────────────────────┐
│ Mark.~FMemMark() 被调用 │
│ ↓ │
│ Mark.Pop() │
│ ↓ │
│ FMemStack::Top = Mark 保存的位置 │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [ 全部变成可用空间 ] │ │
│ │ ↑ │ │
│ │ Top (恢复到 Mark 创建时的位置) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 注意:TArray 的析构函数仍会被调用, │
│ 但 TMemStackAllocator 的"释放"操作是空操作 │
│ 真正的内存回收由 FMemMark::Pop() 完成 │
└─────────────────────────────────────────────────────────────┘
⚠️ TMemStackAllocator 使用注意事项
| 注意点 |
说明 |
| 🚫 不能返回 |
不能把 TArray<T, TMemStackAllocator<>> 作为返回值,函数结束后内存就无效了 |
| 🚫 不能存储 |
不能把它存到类成员变量中 |
| ✅ 仅限局部 |
只在当前作用域内使用 |
| ✅ 可以传引用 |
可以传给子函数使用(只要 Mark 还活着) |
📈 性能优势对比
cpp
TArray<int32> NormalArray;
NormalArray.Reserve(1000);
NormalArray.Reserve(2000);
TArray<int32, TMemStackAllocator<>> StackArray;
StackArray.Reserve(1000);
StackArray.Reserve(2000);
6. 性能对比与最佳实践
📊 性能对比测试
cpp
void PerformanceComparison()
{
const int32 NumAllocations = 10000;
const int32 AllocationSize = 256;
{
double StartTime = FPlatformTime::Seconds();
for (int32 i = 0; i < NumAllocations; i++)
{
void* Ptr = FMemory::Malloc(AllocationSize);
FMemory::Free(Ptr);
}
double EndTime = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("标准 malloc/free: %.4f ms"),
(EndTime - StartTime) * 1000.0);
}
{
double StartTime = FPlatformTime::Seconds();
FMemMark Mark(FMemStack::Get());
for (int32 i = 0; i < NumAllocations; i++)
{
void* Ptr = FMemStack::Get().Alloc(AllocationSize, 16);
}
double EndTime = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("FMemStack: %.4f ms"),
(EndTime - StartTime) * 1000.0);
}
}
📈 典型性能数据
| 操作 |
malloc/free |
FMemStack |
提升倍数 |
| 10000次分配+释放 |
~2.5 ms |
~0.15 ms |
~17x |
| 内存碎片 |
有 |
无 |
∞ |
| 缓存友好性 |
差 |
优秀 |
- |
| 线程安全开销 |
有锁竞争 |
线程本地 |
- |
✅ 最佳实践清单
cpp
void GoodPractice()
{
FMemMark Mark(FMemStack::Get());
void* Data = FMemStack::Get().Alloc(1024, 16);
}
void BadPractice()
{
void* Data = FMemStack::Get().Alloc(1024, 16);
}
void ContainerWithMemStack()
{
FMemMark Mark(FMemStack::Get());
TArray<int32, TMemStackAllocator<>> TempArray;
TempArray.Reserve(1000);
for (int32 i = 0; i < 1000; i++)
{
TempArray.Add(i);
}
}
class FMyObject
{
void* DangerousPointer;
TArray<int32> SafeArray;
};
7. 常见问题与注意事项
⚠️ 陷阱1:忘记创建 Mark
cpp
void ForgotMark()
{
void* Ptr = FMemStack::Get().Alloc(100, 16);
}
check(!bShouldEnforceAllocMarks || NumMarks > 0);
⚠️ 陷阱2:Mark 顺序错误
cpp
void WrongMarkOrder()
{
FMemMark Mark1(FMemStack::Get());
FMemMark Mark2(FMemStack::Get());
Mark1.Pop();
Mark2.Pop();
}
void CorrectMarkOrder()
{
FMemMark Mark1(FMemStack::Get());
FMemMark Mark2(FMemStack::Get());
}
⚠️ 陷阱3:跨线程使用
cpp
void CrossThreadDanger()
{
FMemMark Mark(FMemStack::Get());
int32* Data = new(FMemStack::Get()) int32[100];
AsyncTask(ENamedThreads::AnyThread, [Data]()
{
Data[0] = 42;
});
}
void SameThreadSafe()
{
FMemMark Mark(FMemStack::Get());
int32* Data = new(FMemStack::Get()) int32[100];
ProcessData(Data, 100);
}
⚠️ 陷阱4:析构函数不会被调用
cpp
class FMyClass
{
public:
~FMyClass() { UE_LOG(LogTemp, Log, TEXT("析构了")); }
};
void DestructorNotCalled()
{
FMemMark Mark(FMemStack::Get());
FMyClass* Obj = new(FMemStack::Get()) FMyClass();
}
void ManualDestruct()
{
FMemMark Mark(FMemStack::Get());
FMyClass* Obj = new(FMemStack::Get()) FMyClass();
Obj->~FMyClass();
}
📋 使用检查清单
| 检查项 |
说明 |
| ✅ 创建了 FMemMark |
分配前必须有活跃的 Mark |
| ✅ Mark 在正确作用域 |
Mark 的生命周期覆盖所有使用 |
| ✅ 同一线程使用 |
不要跨线程传递 MemStack 指针 |
| ✅ 不存储到长期对象 |
MemStack 内存是临时的 |
| ✅ 大小合理 |
单次分配不要超过 PageSize |
| ✅ POD 类型优先 |
避免需要析构函数的复杂类型 |
🎓 总结
FMemStack 核心要点
┌─────────────────────────────────────────────────────────────┐
│ FMemStack 知识图谱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📦 组件 🎯 特点 │
│ ├─ FPageAllocator ├─ O(1) 分配速度 │
│ │ └─ 64KB/1KB 页面 ├─ 零碎片 │
│ ├─ FMemStackBase ├─ 线程本地 │
│ │ └─ 线性分配器 ├─ 批量释放 │
│ ├─ FMemMark └─ 缓存友好 │
│ │ └─ 作用域管理 │
│ └─ TMemStackAllocator │
│ └─ 容器适配器 │
│ │
│ ✅ 适用场景 ❌ 不适用场景 │
│ ├─ 帧内临时数据 ├─ 长期存储 │
│ ├─ 渲染收集 ├─ 跨线程共享 │
│ ├─ 算法临时空间 ├─ 需要单独释放 │
│ └─ 批处理操作 └─ 大小不确定的增长数据 │
│ │
└─────────────────────────────────────────────────────────────┘
🚀 一句话记住
FMemStack = 便签本 + 书签 📝🔖
快速撕页(分配),做好书签(Mark),一次性扔掉(Pop)!
📚 相关源码文件
| 文件 |
路径 |
说明 |
| MemStack.h |
Engine/Source/Runtime/Core/Public/Misc/MemStack.h |
主要头文件 |
| MemStack.cpp |
Engine/Source/Runtime/Core/Private/Misc/MemStack.cpp |
实现文件 |
文档版本:1.0 | 基于 Unreal Engine 源码分析