Heap ≈ Allocator + Arena
Allocator
是「怎么分内存」的逻辑;Arena
是「从哪分内存」的资源;Heap
是这两者的组合体,能实际运行;特征 | 意义 |
---|---|
手动管理内存 | 所有分配、释放都要手动,小心碎片 |
自定义数据结构 | 替代 STL,自控内存和行为 |
无操作系统依赖 | 程序必须独立完成所有任务 |
高效与可预测性 | 所有设计追求低开销、可测性能 |
代码更像 C 而不是现代 C++ | 结构体 + 函数方式主导 |
“C style 2000s Interfaces for speed” 这种风格强调性能优先、手动管理内存、宏定义抽象、最小化虚函数等开销,适用于嵌入式开发、游戏引擎(如早期的 Unreal Engine、id Tech)等对性能极端敏感的领域。
#define NEW_DELETE_OPERATORS(debug_name)
是什么?这是一个宏定义(macro),它为类统一地重载 new
和 delete
运算符。宏的作用是让你在每个类里轻松加上这些重载,而不用复制粘贴冗长的代码。
可能的宏定义如下(你贴的代码没给出具体内容,但这是惯用写法):
#define NEW_DELETE_OPERATORS(debug_name) \
void* operator new(size_t size) { \
return MyAllocator::Allocate(size, debug_name); \
} \
void operator delete(void* ptr) { \
MyAllocator::Deallocate(ptr); \
}
debug_name
用于记录是哪一类在调用内存分配,方便调试内存泄漏或碎片。malloc
/free
或 new
/delete
频繁调用造成的性能问题。class CollisionChooser {
public:
NEW_DELETE_OPERATORS(CollisionChooser)
...
};
这一段代码意思是:
CollisionChooser
类重载了 new
和 delete
。MyAllocator
或其他机制。CollisionChooser
会作为一个“标签”传入,标记该对象是哪个类分配的。std::allocator
,而非手动宏和内存管理。你贴的内容描述的是早期 C++ 项目中通过宏定义和手动内存管理提升性能的做法。NEW_DELETE_OPERATORS(debug_name)
是一个宏,用来给类重载 new
和 delete
运算符,通常用于配合固定大小内存池以获得更快的内存分配和释放。
如果你想,我也可以帮你实现这个宏的完整写法,或者改写成现代 C++ 的等效写法(比如使用 allocator + std::make_unique)。要看你是想继续沿用旧风格还是现代化。
operator new
实现对内存分配的追踪与验证,包括记录分配来源(debug_name)、使用 flags 控制分配策略,并通过内存块中的 footer 存储调试信息。Global new
重载void* operator new(size_t size, const char* debug_name, int flags = MB_LOW);
这是一种 全局重载的 operator new
,增加了调试信息的参数:
debug_name
: 一个字符串,用来标识这个内存块是由谁分配的。flags
: 用于控制分配行为,比如是否来自低内存池 (MB_LOW
) 或其他内存区。你提到的:
Allocated Block Header footer
Allocated Block Header footer
Allocated Block Header footer
表示每个分配的内存块不是只包含用户数据,而是:
[Header][User Data][Footer]
Header
: 一般存储 size、flags、校验信息、分配堆栈等。User Data
: 真正给程序使用的部分。Footer
: 存放 debug_name
,哨兵(sentinel)等。Footer
中的调试信息(如 “render::player”)。debug_name
的使用:分类字符串标记来源debug_name split into category::alloc
Examples:
"render::player"
"gameplay::physicsmesh"
这是一种 命名规范,用来把内存分配归类,便于调试分析:
render::player
表示渲染子系统分配给玩家的对象。gameplay::physicsmesh
表示游戏逻辑中的物理网格分配。你看到的这一风格,是 2000 年代在大型项目中常用的做法,尤其在游戏引擎、嵌入式系统、图形引擎中:
特征 | 说明 |
---|---|
自定义 operator new |
增加调试信息参数 |
内存块加头尾部 | 记录元信息和检测内存破坏 |
debug_name 命名规范 |
类似命名空间,用于归类调试 |
flags 参数 |
控制使用哪种内存策略(如低地址池) |
我可以提供完整的示例代码,例如:
struct BlockHeader {
size_t size;
int flags;
// ...
};
struct BlockFooter {
const char* debug_name;
uint32_t sentinel;
};
void* operator new(size_t size, const char* debug_name, int flags = MB_LOW) {
size_t total = size + sizeof(BlockHeader) + sizeof(BlockFooter);
void* raw = malloc(total);
BlockHeader* header = (BlockHeader*)raw;
header->size = size;
header->flags = flags;
void* userPtr = (void*)(header + 1);
BlockFooter* footer = (BlockFooter*)((char*)userPtr + size);
footer->debug_name = debug_name;
footer->sentinel = 0xDEADBEEF;
return userPtr;
}
我来帮你分段解释和总结:
Almost all memory is in one heap
Well we did have a simple small block allocator
We had to work hard at defragmentation
Low Memory
├── Small Block Allocator
│ ├── Decompress
│ ├── Alloc
│ └── Mesh
│
└── Load compressed Texture 0
High Memory
└── General Allocator
└── Texture 0
Decompress
: 解压缩缓冲区(临时内存)Alloc
: 小对象分配(可能是游戏逻辑对象)Mesh
: 小模型加载或中间数据结构Texture 0
: 贴图、大型资源缓冲区技术 | 解释 | 优点 |
---|---|---|
单一主堆 | 所有内存统一调度 | 简化结构 |
小块分配器 | 为频繁分配的小对象设计 | 快、低碎片 |
手动 Defrag | 需要程序员设计内存迁移和整理机制 | 保持长时间运行稳定性 |
地址分层 | 将低/高地址分区,绑定资源类型 | 有助于平台优化(如主机 GPU DMA) |
假设你在开发一款游戏:
DefragmentHeap()
手动整理内存。这段内容描述的是早期(2000年代)程序在内存分配方面的策略:
2004 - Xbox 360, PS3 (512MB RAM)
Virtual memory! - NO HDD , No GPU support, 32-bit
“不仅仅是 Sega Saturn 了 ”
The main changes for 2005:
Support for multiple allocators
Better tracking and logging tools
Stomp allocator!!
Memory tracking with EASTL
callstack
debug_name
、分配时间、生命周期等[Guard Page][Allocated Block][Guard Page]
一旦代码非法访问了这些 guard page,系统就会立刻抛出崩溃,比传统调试手段更早发现问题。
eastl::vector<MyType, MyCustomAllocator> myVector;
这样每个容器都可以明确指定 allocator,配合内存追踪使用,实现精准定位。
2005 年游戏主机时代内存管理的主要变革如下:
项目 | 内容 | 目的 |
---|---|---|
多分配器支持 | 各类内存按功能分离管理 | 减少碎片、提升性能 |
日志和追踪工具 | 更好调试和内存使用分析 | 查泄漏、优化分布 |
Stomp Allocator | 分配器带保护页 | 抓内存越界/悬挂指针 |
EASTL 跟踪支持 | 容器与 allocator 深度绑定 | 避免隐式 heap 分配 |
ICoreAllocator*
)实现对象的构造与析构,而不是使用普通的 new
和 delete
。下面来一步步详细解释:
SQLQuery* NewQuery(ICoreAllocator* a) {
return CORE_NEW(a, "sql", MEM_LOW) SQLQuery(a);
}
void DeleteQuery(ICoreAllocator* a, SQLQuery* sql) {
CORE_DELETE(a, sql);
}
特性 | 含义 |
---|---|
ICoreAllocator* a |
多态分配器接口,表示你可以传入不同的分配器(frame allocator, pool allocator, heap allocator 等) |
CORE_NEW(...) Type(args...) |
宏封装的 placement new,结合 allocator 分配内存 + 构造对象 |
CORE_DELETE(...) |
宏封装的:调用析构函数(~Type)+ allocator 回收内存 |
不使用 delete |
不走默认全局 operator delete ,而是自定义内存系统负责销毁 |
传统写法:
SQLQuery* q = new SQLQuery(); // 调用全局 new
delete q; // 调用全局 delete
frame_allocator
:一帧用完就丢pool_allocator
:快速、适合大量同类对象debug_allocator
:用于调试时追踪"sql" // 这个是 debug_name,可以在日志中记录
MEM_LOW // 分配 flags,告诉系统:低地址优先、小内存池
CORE_DELETE
不只是释放指针,它会:
~SQLQuery()
析构函数(非 delete
)a->Free()
来回收内存delete
的不确定行为假设定义如下宏:
#define CORE_NEW(alloc, name, flags) \
new ((alloc)->Alloc(sizeof(SQLQuery), name, flags))
#define CORE_DELETE(alloc, ptr) \
if (ptr) { (ptr)->~SQLQuery(); (alloc)->Free(ptr); }
这样就可以解释:
SQLQuery* q = CORE_NEW(a, "sql", MEM_LOW) SQLQuery(a);
等价于:
void* mem = a->Alloc(sizeof(SQLQuery), "sql", MEM_LOW);
SQLQuery* q = new (mem) SQLQuery(a); // placement new 构造对象
因为:
delete ptr;
默认会:
operator delete
释放内存(通常是全局堆)你贴的代码体现了 2005 年游戏/引擎编程中,支持多种内存分配器(multiple allocators)的一种常见写法:
ICoreAllocator*
控制分配来源CORE_NEW
/CORE_DELETE
宏包装 placement new 和显式析构new
/ delete
ICoreAllocator
接口 + 一个实际 allocator 实现 + 宏用法的最小工作例子(可编译)吗?这可以让你真正“跑起来”看它如何工作。下面我逐条解释你贴的内容。
“A mix of time and size gives good defragmentation properties.”
在 2005 年的游戏开发中,内存组织要面对的问题包括:
Render Heap
Gameplay Heap
UI Heap
每个子系统(模块)有自己专属的 heap 和 SBA(Small Block Allocator):
模块 | 描述 |
---|---|
Render Heap | 用于渲染系统分配资源,比如纹理、渲染状态缓存等 |
Gameplay Heap | 游戏逻辑使用,如实体、AI、碰撞体等 |
UI Heap | 用户界面层的内存,如菜单、文本框、按钮对象等 |
SBA | 每个模块都有自己的 small block allocator,优化小对象 |
这样做的好处: |
Small / Medium / Large
这层是典型的 slab allocator / segregated size-class 策略:
大小 | 举例 |
---|---|
Small | 16B~128B(指针、字符串、组件对象) |
Medium | 512B~2KB(纹理片段、小mesh、节点) |
Large | >8KB(大纹理、全局缓存) |
好处: |
Static / Level / Global / SubLevel / Time
这是一种按“对象寿命周期”来分配堆的方法:
类别 | 生命周期 | 示例 |
---|---|---|
Static | 永久存在 | 全局配置、字体缓存 |
Level | 一局游戏/关卡 | 角色数据、碰撞树 |
SubLevel | 场景片段 | 动画段、子地图 |
Time | 一帧/一小段时间 | 粒子、AI临时路径点 |
好处: |
“Organizing by team fragments heaps but easy to set blame.”
可以根据开发团队或子系统将堆隔离,例如:
团队 | 对应堆 |
---|---|
渲染组 | RenderHeap |
游戏逻辑组 | GameplayHeap |
网络组 | NetHeap |
好处: |
这段内容讲的是 如何多维度组织内存堆(Heap)/ 区域(Arena) 来满足游戏开发复杂场景中的性能、调试、碎片控制需求:
维度 | 分类方式 | 优点 |
---|---|---|
按功能模块 | Render / Gameplay / UI Heap | 易于调试、分隔清晰 |
按分配大小 | Small / Medium / Large | 减少碎片、提升复用 |
按生命周期 | Static / Level / SubLevel / Time | 整块释放、低开销 |
按团队 | 各团队独立 Heap | 容易追责、监控预算 |
最终作者说: |
“我们团队会根据实际需要混合使用这些策略。”
这也是最理智的方式 —— 灵活地根据资源、内容、平台、目标设备调整内存组织方式。
Memory Corruption between teams sucks
Fragmentation between teams is hard.
Who to blame when you are out of memory?
Render Heap
Simulation Heap
UI Heap
每个团队独立分配自己一块内存区域(Arena),只有这个团队能在其中分配对象。
“Categories are a way to tag allocations so you can budget them together.”
这是更灵活的方法:
CORE_NEW(a, "render::shadowmap", MEM_LOW) ShadowMap(a);
或Alloc(size, "gameplay::projectile", MEM_TEMP);
很多项目团队在 2005 年(甚至今天)采用的做法是:
render::meshcache
| 25 MB | 32MB | 210 |gameplay::npc
| 12 MB | 18MB | 1,224 |ui::popupmenu
| 2 MB | 2MB | 42 |方案 | 描述 | 优点 | 缺点 |
---|---|---|---|
Team-Based Heaps | 每个团队/系统一个专属内存区域 | 隔离好,责任明确 | 容易碎片,不好复用 |
Category-Based 分配 | 给每次分配打上 tag 分类 | 易追踪,灵活,统一调度 | 越界难追责,需要更强工具支持 |
作者最终推荐的是使用 category tagging 来进行统计和预算管理,同时保留部分 arena 来隔离关键高风险模块。 |
在 Debug 模式下,更好地追踪每一次分配和释放,以便:
[ Header ][ User Data ][ Footer Sentinel ]
H - Header
F - Footer(如 0xDEADBEEF)
render::meshdata
|gameplay::enemy
|Alloc(size, "category::name")
[Header][UserData][FooterSentinel]
logTable.Add({
ptr: 0x1000,
size: 128,
tag: "gameplay::npc"
});
Free(ptr)
时,自动在 Debug Heap 中移除对应记录+ [ALLOC] 0x123456 Size: 128 Tag: gameplay::npc
+ [ALLOC] 0x1234F0 Size: 64 Tag: render::shadowmap
- [FREE ] 0x123456
“Only sentinel stored in footer”
*(end_of_alloc) = 0xDEADC0DE;
free
时检查 sentinel 是否被改写特性 | 描述 | 目的 |
---|---|---|
Sentinel in Footer | 在每个块尾部加一个哨兵值 | 检测越界写入 |
Debug Heap | 单独存储所有分配信息(地址、大小、tag) | 查内存泄漏、追踪来源 |
Category Tag | render::player , gameplay::npc |
细粒度内存分类,易调试 |
Memory Logging | 日志写入内存或磁盘 | 离线分析、自动化测试 |
Live Allocation Tracking | 保持当前活跃内存状态 | 实时检测泄漏和峰值 |
如你有兴趣,我可以帮你写一个简单的 C++ 模拟实现,包括: |
下面是详细解释:
“whole snapshot of memory”
每个时间点系统会记录一份完整内存快照,包括:
“delta between 2 times”
当你选择两个时间点(如 A→B),系统会计算这段时间内:
项目 | 含义 |
---|---|
Alloc Count | 新增了多少次分配 |
Alloc Size | 总共增加/减少了多少内存 |
Category Changes | 哪些 category 增加最多 |
Leaked Blocks | 哪些分配在 A 时有但 B 时还没释放 |
这就是所谓的 “内存差异分析” 或 delta report: | |
Category | ΔCount |
----------------- | ------ |
render::mesh |
+120 |
gameplay::enemy |
+12 |
ui::tooltip |
-8 |
alloc
有没有“持续不释放”概念 | 解释 |
---|---|
Memory Snapshot | 记录某一时刻完整的内存状态 |
Time Selection | 选择任意两个时间点进行对比分析 |
Delta Report | 比较两次快照的变化,包括分配次数和总大小 |
Category Breakdown | 分析是哪些系统或类别的分配增长 |
日志输出 | 写入磁盘或调试工具,可用于可视化分析 |
这正是现代内存分析器(如 UE4 的 Memory Insights、Unity Profiler、RenderDoc)等工具的雏形。 |
它是一个图形化调试工具,显示一个内存 Arena(或 Heap)里的每个内存块,包括:
颜色 | 含义说明 |
---|---|
绿色 Green | 系统(如 gameplay/render)正在使用的块 |
黄色 Yellow | 当前选中的块(用于查看详细信息) |
紫色 Purple | 演示系统用的内存块(如 UI、视频缓冲等) |
⬜ 灰色 Grey | 空闲的(free)内存块 |
Address: 0x12ACF000
Size: 256 bytes
Tag: gameplay::npc
Allocator: SmallBlockArena
Age: 5.2 seconds
[⬜⬜⬜⬜] (8KB blocks)
元素 | 含义 |
---|---|
Arena | 一块内存区域,通常按用途分,如 RenderArena、GameArena |
Block | Arena 中的一个内存块,可能被使用或空闲 |
颜色标记 | 帮助快速区分谁用了哪些内存 |
可视化作用 | 显示内存使用布局,找碎片、查泄漏、优化性能 |
调试工具价值 | 是早期内存可视化调试器的重要组成部分 |
“Stomp Allocator” 是一种调试用内存分配器,它会在分配内存时故意制造条件,一旦代码写出边界就立刻崩溃(crash),以便在开发阶段快速发现内存越界 Bug。
每次分配都使用两页内存(例如 4KiB 一页):
[ Page 1 - Read/Write ] ← 用来存用户数据,前面一部分可用
[ Page 2 - ReadOnly / 未映射 ] ← 设置成不可写的保护页
比如:
|----------------| ← 4KiB RW 页
| [User Alloc] |
| 512B |
|----------------| ← 边界
| Guard Page | ← 4KiB 保护页(只读或根本没映射)
char* p = (char*)StompAlloc(512);
p[100] = 42; // OK
p[1024] = 99; // 超过 512B,触发写到下一页
// 下一页是“只读”或“未映射”
// 程序立即崩溃,定位精确
特点 | 说明 |
---|---|
立即崩溃 | 一旦越界写,程序马上 crash,定位精确 |
替代 sentinel | 比“哨兵值”更主动更强力 |
Debug 极有效 | 越界 Bug 很难发现,stomp 让它们“一击即中” |
利用虚拟内存保护机制 | 分配整页,操作系统级别防止非法访问 |
问题 | 原因 |
---|---|
非常浪费内存 | 每个小对象也要分配 4KiB(甚至 8KiB) |
性能开销高 | 页表、保护页修改开销大,不适合 release 模式 |
不能批量使用 | 不能用于大批量小对象的实际运行时分配,只能用于调试特定问题 |
“Use sentinel? Or Flip?”
[ Guard ] [ Alloc ] [ Guard ]
点 | 说明 |
---|---|
Stomp Allocator | 通过虚拟内存页保护实现越界写检测 |
原理 | 一页可写,一页只读/未映射 |
效果 | 越界写直接崩溃,方便定位 |
适用场景 | 调试阶段单元测试 / 找 Bug |
不适合 | 运行时、性能敏感场合 |
shared_ptr
)“Add a debug system for ref counts is hard”
Sim → Player → ParticleSystem
Render → Player → Collision → Mesh
如果 Mesh 没释放,是因为哪个 Player 没释放?哪个系统保留了引用?很难定位。
“A tracking system would be like garbage collector…”
“A Logging system would generate even more data…”
shared_ptr
是有用的“shared_ptr are useful !!”
替代方案 | 优点 |
---|---|
unique_ptr |
生命周期简单、不会共享引用,适合局部逻辑 |
✴ 裸指针(bare ptr) | 更明确控制何时释放、更清晰所有权(适合短生命周期或静态对象) |
情况 | 推荐做法 |
---|---|
短生命周期,明确所有权 | unique_ptr |
无法明确所有权 / 多模块共享 | shared_ptr |
生命周期由全局控制 / 不拥有 | 裸指针 + 手动管理 |
项目 | 说明 |
---|---|
引用计数指针难调试 | 无法追踪谁加/减了引用,泄漏难找 |
引用跟踪像 GC | 会引入复杂的运行时开销 |
日志会爆炸 | 引用变动频繁,log 数据量大 |
建议 | shared_ptr 有用,但优先考虑 unique_ptr 或裸指针以简化生命周期 |
EASTL 是由 EA(Electronic Arts)为游戏开发专门设计的一套 C++ 模板库,目的是替代标准 STL,以满足游戏开发中对性能、内存控制、调试友好性的更高要求。
“STL allocators are painful to work with”
问题 | 原因 |
---|---|
分配器接口复杂 | allocator 类型难用,修改 allocator 很痛苦 |
容器不可插入调试信息 | STL 没办法轻松标记 debug name 或分配来源 |
不支持 intrusive 容器 | 比如链表必须自己管理节点,STL 不允许这样 |
不可预测的分配行为 | 有些容器内部自己偷偷 new 内存,难以控制内存来源 |
没有 ring buffer 等 | 标准库功能不够多样,需手动实现一些常用结构 |
EASTL 特性 | 意义 |
---|---|
自定义 allocator 系统 | 可以把 Arena、Heap、标签名都插进去 |
容器支持调试信息 | 可以传入 debug_name (比如 render::player ) |
支持 intrusive 容器 | 低开销、高性能,适合内存池、链表管理等 |
支持 RingBuffer 等扩展容器 | 提供游戏常用的容器而不是通用场景 |
更好性能 | 没有多余的虚函数、异常支持,专为性能优化 |
更可控初始化行为 | 可以控制对象是否构造、何时分配内存 |
“Memory is allocated in empty versions of some STL objects”
这是对 标准 STL 的批评,例如:
std::string s;
在标准 STL 中,即使 string 是空的,也可能已经分配了 heap 内存!这会影响:
“A 2010 version of EASTL is available now from webkit”
项目 | 说明 |
---|---|
EASTL 目的 | 替代 STL,提升性能、内存可控性 |
为什么不用 STL | 分配器不友好,调试难,不支持游戏常用容器 |
EASTL 优势 | 支持自定义 allocator,支持 debug 标签,性能好 |
特别点 | 避免 STL 的“空对象分配内存”等问题 |
状态 | 2010 以后对外开源,有 GitHub 可用版本 |
你看到的是 EASTL 和 STL 在 188 个性能测试中的比较结果:
测试结果 | 含义 |
---|---|
EASTL 更快:71 次 | EASTL 速度达到或超过 STL 的 1.3 倍 |
EASTL 更慢:10 次 | EASTL 的速度只有 STL 的 0.8 倍或更差 |
其他(107 次) | EASTL 和 STL 差别不大(中间地带) |
“Faster means 1.3x or better.”
“Slower means 0.8x as quick or slower.”
也就是说:
内容 | 理解 |
---|---|
EASTL 在多数场景略快 | 在 71 次测试中 ≥1.3x,适合对性能敏感的系统 |
有些情况 EASTL 也可能慢 | 在 10 次中 ≤0.8x,表明某些 STL 优化场景可能更成熟 |
多数场景下两者性能差不多 | 在 100 多次测试中两者表现接近 |
EASTL 更适合游戏/实时系统优化 | 它的设计初衷就是针对这类高性能需求 |
EASTL 相对 STL:
测试结果 | 数量 | 含义 |
---|---|---|
EASTL 更快 | 164 | EASTL 执行时间是 STL 的 1.3 倍更快或更好 |
表现差不多 | 19 | EASTL 和 STL 差异很小 |
EASTL 更慢 | 2 | EASTL 明显慢(≤ 0.8x) |
STL 在 Debug 模式下启用了大量“安全检查”,例如:
STL Debug 模式特性 | 后果 |
---|---|
iterator 检查 | 每次迭代都要验证合法性 → 性能大幅下降 |
bounds 检查 | 访问元素要做越界检测 |
拷贝构造/析构频繁调用 | 模拟真实运行 → 更慢 |
分配器使用不统一 | 可能频繁堆分配 |
EASTL 设计就是为**性能敏感场景(特别是游戏引擎)**打造:
EASTL 优化点 | 效果 |
---|---|
没有 iterator 检查 | 提高迭代性能 |
无额外 Debug 辅助开销 | 不在 Debug 模式里做 STL 那些慢操作 |
支持自定义 allocator | 分配速度快,避免堆碎片 |
容器结构轻量 | 结构简单,拷贝/移动更快 |
点 | 说明 |
---|---|
EASTL 在 Debug 模式中压倒性更快 | 在 164 / 188 次测试中胜出 |
STL 在 Debug 模式有大量额外负担 | 使得测试极慢,不适合调试高性能系统 |
EASTL 几乎没有 Debug 限制逻辑 | 使得 Debug 构建更接近 Release 行为 |
EASTL 更适合游戏和嵌入式开发 | 因为 Debug 构建也要能流畅运行并测试帧率 |
如果你在做游戏引擎、实时仿真系统或控制系统:
信息点 | 含义 |
---|---|
EA 准备开源 EASTL | Electronic Arts 决定让 EASTL 对外开源 |
Roberto Parolin 接受 PR | EA 的工程师 Roberto Parolin 将负责维护并接受社区提交(pull request) |
GitHub 地址 | 代码托管在 EA 的 GitHub: https://github.com/electronicarts |
技术细节稍后公布 | 细节将在 C++ 标准委员会的 SG14 小组中发布(SG14 是专注于“低延迟和嵌入式系统”的 C++ 小组) |
总结: EASTL 计划开源,将接受社区改进并持续维护,是游戏开发者和嵌入式系统开发者的重大利好。 |
这部分说明 EASTL 在早期版本中(2005年)使用 allocator 跟踪内存分配比较麻烦。
比如使用 eastl::vector
时:
typedef eastl::vector<int, EASTLICoreAllocator> MyVec;
你要:
ICoreAllocator* alloc = GetGameplayAllocator();
MyVec vec(alloc);
问题 | 原因 |
---|---|
每个容器都要单独 typedef | 增加代码复杂度,无法复用模板通用代码 |
必须手动传 allocator | 增加出错可能(忘记传/传错) |
无法轻松做统一分配跟踪 | allocator 无法自动携带 debug name / category |
测试或日志系统难以集成 | 无法自动知道某个 vector 是属于哪个模块 |
现代 EASTL 引入了更自动化的 allocator 架构,例如:
EASTLAllocatorType allocator("render::player");
eastl::vector<int> v(allocator);
并且使用 默认全局 allocator + 分配器标签信息(debug name)能自动帮你追踪分配来源。
内容 | 说明 |
---|---|
EASTL 将开源 | 托管在 EA GitHub 上,未来可参与开发 |
早期 EASTL allocator 使用繁琐 | 每个容器要指定 allocator 类型和构造时传入实例 |
原因 | 为了性能与分配器自定义,但导致调试与跟踪难度高 |
后续改进方向 | 更智能的 allocator 接口,支持 debug name 和自动分配器识别 |
以 unordered_map
为例:
unordered_map(
size_type n = 1000,
const hasher& hf = hasher(),
const key_equal& eql = key_equal(),
const allocator_type& alloc = allocator_type() // 默认 allocator 参数
);
问题 | 说明 |
---|---|
用户容易忽略传 allocator 参数 | 因为默认参数存在,用户可能不传,分配器就不是自定义的 |
内存跟踪失效 | 不能保证每个容器都用正确的、可跟踪的 allocator |
虽然能用,但很麻烦 | 需要额外调用接口,手动替换 allocator |
EA::ICoreAllocator* alloc = GetRendAllocator();
vec.get_allocator().set_allocator(alloc);
get_allocator().set_allocator()
手动设置 allocator。现象 | 说明 |
---|---|
EASTL 容器构造函数默认 allocator 参数 | 导致很难强制用户传自定义 allocator |
内存分配跟踪因此困难 | 容器可能用了默认 allocator,跟踪不准确 |
只能通过手动调用 set_allocator 解决 |
但使用体验差,容易漏掉或写错 |
vector v(eastl::allocator("AI::Piano::Input"));
vector
时传入带有**字符串标识(debug 名称)**的 allocator。问题 | 说明 |
---|---|
不同团队代码难以共享 | 因为每个团队可能传入不同的 allocator 名称,导致容器和分配器强耦合 |
用字符串作为 allocator 标识不靠谱 | 运行时字符串查找、易出错,且难以统一管理和跟踪 |
这种 hack 只是临时方案 | 不利于长期维护和跨团队协作 |
结论 | 说明 |
---|---|
直接通过带名字的 allocator 构造容器是简便但不健壮 | 增加团队间依赖和代码复杂度 |
更好的方案是用统一的 allocator 管理和共享机制 | 避免字符串管理带来的问题 |
typedef vector<int, EASTLICoreAllocator> MyVec;
typedef vector<int> YourVec;
MyVec myVec;
YourVec yourVec;
myVec = yourVec; // 出错!
MyVec
是带有自定义 allocator 的 vector
。YourVec
是默认 allocator 的 vector
。原因 | 说明 |
---|---|
模板类型不同导致不兼容 | vector 和 vector 被视为完全不同的类型 |
C++ 没有内置“类型擦除”机制允许不同 allocator 的容器相互赋值 | 编译器报错:没有适合的赋值运算符或转换 |
这使得跨 allocator 的容器操作复杂 | 不能简单地把带有一个 allocator 的容器赋值给带有另一个 allocator 的容器 |
现象 | 说明 |
---|---|
不同 allocator 的容器类型不同 | 导致赋值操作编译失败 |
缺少类型擦除机制 | 不能轻松让容器间互换数据 |
这给内存跟踪和代码复用带来麻烦 | 需要开发者手动管理不同类型容器 |
ICoreAllocator*
),方便管理和跟踪。String
。String
继承自 EASTL 的 base_string
,并固定 allocator 类型为 EASTLICoreAllocator
。ICoreAllocator*
和一个字符串名字(用于调试/标记)。EASTLICoreAllocator
实例(传入名字和 allocator 指针)传给基类。template <typename T>
class String : public base_string<T, EASTLICoreAllocator> {
public:
String(ICoreAllocator* alloc, const char* name = "Str")
: base_string<char, EASTLICoreAllocator>(
EASTLICoreAllocator(name, alloc))
{
// 其他初始化
}
};
EASTLICA::String
就必须传入一个 ICoreAllocator*
。ICoreAllocator* alloc = GetStringAllocator();
EASTLICA::String str(alloc);
优点 | 说明 |
---|---|
统一 allocator 接口 | 所有 EASTLICA 容器都用 ICoreAllocator* 管理 |
强制分配器传递,避免默认 allocator | 减少忘传或误用问题 |
方便调试和内存跟踪 | 传入调试名字,能更精准地定位内存来源 |
团队共享代码更简洁 | 代码风格统一,便于维护 |
#define EASTLICA_VECTOR( EASTLICA_TYPE, GET_DEFAULT_ALLOC, ALLOC_NAME ) \
template<typename T> class EASTLICA_TYPE : public EASTLICA::Vector<T>
EASTLICA_TYPE
,它继承自 EASTLICA::Vector
。EASTLICA::Vector
是封装了多态 allocator 的 EASTL vector。EASTLICA_STRING( CareerModeString,
CareerMode::GetStringDefaultAllocator(), "CareerStr");
CareerModeString
的 EASTLICA string 类型。"CareerStr"
。作用 | 优点 |
---|---|
用宏快速生成类型定义 | 减少重复代码,写法统一 |
每个大系统用独立类型 | 清晰标识不同子系统的 allocator |
简化 allocator 绑定 | 容器和 allocator 一体化管理 |
CareerModeString str;
LocalizedString lstr = getStrId(42);
str = lstr; // 编译通过!
CareerModeString
和 LocalizedString
都是用 相同的 allocator 类型(通过 EASTLICA 宏定义实现的)。解决的问题 | 说明 |
---|---|
类型擦除(type erasure) | 统一 allocator 类型后,容器赋值兼容 |
所有权管理 | 支持不同子系统对字符串的拥有权差异 |
灵活的 allocator 复制 | 允许根据需求决定 allocator 是否拷贝 |
这就是 EASTLICA 通过封装 allocator 带来的最大改进:统一内存管理接口,提升代码复用和安全性,同时支持复杂的所有权模型。 |
重点方向 | 说明 |
---|---|
Debug Memory System | 增强的调试内存系统,方便找内存问题 |
EASTL Memory Tracking | 使用 EASTL 的内存跟踪功能,精确统计和定位内存使用 |
新的调试工具 | 更新和增强的工具帮助调试性能和内存问题 |
现代主机环境提供了更大、更复杂的内存系统,软件层面通过 先进的内存跟踪和调试机制 来充分利用硬件优势,减少内存错误和泄漏,提升开发效率和游戏质量。
operator new(size, "MyAlloc")
。void* operator new(size_t size, EA::ICoreAllocator* alloc);
FB_MEMORYTRACKER_SCOPE(data->debugNames[i]);
FB_ALLOC_RES_SCOPE(data->debugNames[i]);
变化点 | 说明 |
---|---|
从单一调试名向作用域扩展 | 作用域能自动绑定更多上下文信息,调试更准确 |
保留老接口但逐步过渡 | 兼容性好,同时推动新模式发展 |
线程本地存储的广泛使用 | 方便多线程追踪,但需注意性能影响 |
class Team {
int teamid;
eastl::vector<player> players;
};
Team* home = new (allocator) Team;
vector
管理玩家列表。Team
对象。虽然大家依旧用 EASTL 容器结合自定义 allocator 进行内存管理,但 EASTL 本身的内存跟踪和调试功能还有不足,这是当下需要改进的地方。
Team Home
是一个整体分配(one allocation),里面有:
int teamId;
eastl::vector players;
players
这个 vector 本身也会分配内存(比如存放 Player 元素的数组)。Gameplay Arena
或 Team Home
的 arena)来做管理。players
的内存分配不一定要用通用 allocator,可以用更专门的:
Gameplay
的小块分配器(Small Block Allocator)关键点 | 说明 |
---|---|
默认使用父 arena 分配 | 避免内存碎片,方便跟踪 |
子 arena 可以灵活切换 | 根据需求选择更合适的 allocator |
形成分配层次结构 | 方便内存调试和性能优化 |
gameplay arena
,后来被移动到 rendering arena
。问题点 | 说明 |
---|---|
跟踪带来的 CPU 开销 | 需要权衡性能和调试需求 |
栈对象无法跟踪 | 只针对堆分配对象 |
移动操作 arena 跟踪困难 | 需要更复杂的策略避免跟踪失效 |
80% 规则 | 大部分场景下简单规则够用,但非全部 |
复杂场景用 EASTLICA | 解决特殊场景下的内存管理难题 |
关键点 | 说明 |
---|---|
DeltaViewer 展示数据 | 通过界面查看内存分配和变化 |
Session 是一次游戏运行 | 分析单次游戏运行的完整内存信息 |
数据传输到 HTTP 服务器 | 实时收集游戏内存数据 |
数据存储为表和视图 | 方便查询、组合和分析内存数据 |
视图名称 | 功能描述 |
---|---|
TTY events debugging | 查看日志事件,调试程序运行流程 |
IO Load profiler | 分析磁盘和资源加载的负载 |
Frame rate & Job profiler | 监控性能和多线程执行效率 |
Memory Investigator | 审查内存泄漏及变化 |
Memory Categorization | 分类统计内存分配,便于资源管理 |
关键词 | 说明 |
---|---|
Bundle | 关卡或子关卡所需文件的集合 |
Chunks | 按需流式加载的数据块(视频、音乐、地形等) |
Timeline | 加载操作的时间轴视图 |
事件线 | 关联打印日志的加载事件,精准定位加载时机 |
Hover | 悬停显示资源详细信息 |
内容点 | 说明 |
---|---|
矩形代表帧 | 高度表示帧耗时(ms),蓝色是选中帧 |
Expensive Frame | 性能瓶颈帧,耗时长的帧 |
Job 和函数调用 | 分析多线程任务和具体函数性能 |
结合加载和帧率分析 | 加载过程涉及 CPU、解压、纹理处理等,不只是磁盘 I/O |
关键点 | 说明 |
---|---|
时间点选取 | 选择关键加载开始和结束时间点对比内存分配情况 |
捕获分配 | 捕获加载期间的所有内存分配 |
比较释放 | 确认加载结束时是否有内存未释放 |
漏洞详情 | 资产名、地址、大小和调用堆栈 |
假漏报问题 | 部分对象增长后自然释放,需结合上下文判断 |