TChunkedArray 深度解析:Unreal Engine 的分块数组容器
1. 概述与设计动机
1.1 什么是 TChunkedArray
TChunkedArray 是 Unreal Engine 中专门设计用于解决大规模数据存储时内存碎片化问题的数组容器。
cpp
// 源码位置: Engine/Source/Runtime/Core/Public/Containers/ChunkedArray.h
/** An array that uses multiple allocations to avoid allocation failure due to fragmentation. */
template<typename InElementType, uint32 TargetBytesPerChunk = 16384, typename AllocatorType = FDefaultAllocator>
class TChunkedArray
1.2 为什么需要 TChunkedArray
TArray 的问题
cpp
TChunkedArray 的解决方案
cpp
1.3 设计理念对比
| 特性 | TArray | TChunkedArray |
|---|---|---|
| 内存布局 | 单一连续内存 | 多个独立 Chunk |
| 扩容机制 | 重新分配 + 拷贝 | 仅分配新 Chunk |
| 扩容复杂度 | O(n) | O(1) |
| 内存碎片敏感度 | 高 | 低 |
| 随机访问 | 直接偏移 | 除法 + 取模 + 二次寻址 |
| 引用稳定性 | 扩容后失效 | 始终稳定 |
| 缓存友好度 | 最优 | Chunk 内良好 |
2. 核心设计原理
2.1 分块存储思想
2.2 模板参数
cpp
template<
typename InElementType, // 元素类型
uint32 TargetBytesPerChunk = 16384, // Chunk 大小(默认 16KB)
typename AllocatorType = FDefaultAllocator // 内存分配器
>
class TChunkedArray
为什么默认 16KB?
cpp
// 16KB = 2^14,是 2 的幂
// 对于 int32(4字节):16384/4 = 4096 = 2^12 元素/Chunk
// 索引计算可优化为位运算:
ChunkIndex = ElementIndex >> 12; // 除以 4096
ChunkOffset = ElementIndex & 0xFFF; // 模 4096
2.3 不同元素大小的 Chunk 容量
| 元素类型 | 大小 | 每 Chunk 元素数 | 是否 2 的幂 |
|---|---|---|---|
int32 |
4B | 4096 | 是 (2^12) |
int64 |
8B | 2048 | 是 (2^11) |
FVector |
12B | 1365 | 否 |
| 128 字节结构 | 128B | 128 | 是 (2^7) |
3. 源码结构分析
3.1 核心数据结构
cpp
3.2 为什么用 TIndirectArray
cpp
// 如果用 TArray<FChunk>:
// 扩容时所有 FChunk 被拷贝 → 元素地址改变 → 引用失效
// 使用 TIndirectArray<FChunk>:
// 扩容时只拷贝指针(8字节)→ FChunk 地址不变 → 引用始终有效
3.3 new FChunk 的构造行为与 POD 类型
什么是 POD (Plain Old Data)
POD 是 C++ 中的术语,指简单旧式数据类型,即像 C 语言中那样的简单数据结构。
cpp
POD 类型的要求:
| 要求 | 说明 |
|---|---|
| 无自定义构造函数 | 只能用默认构造 |
| 无自定义析构函数 | 不能有 ~ClassName() |
| 无虚函数 | 不能有 virtual |
| 无非 POD 成员 | 成员也必须是 POD |
| 无私有/保护非静态成员 | 所有数据成员必须 public |
new FChunk 会调用默认构造吗?
会! 当执行 Chunks.Add(new FChunk) 时:
cpp
struct FChunk
{
ElementType Elements[NumElementsPerChunk]; // 固定大小数组
};
// new FChunk 会:
// 1. 分配 sizeof(FChunk) 字节的内存
// 2. 调用 FChunk 的默认构造函数
// → 对 Elements 数组进行默认初始化
关键区别:
cpp
// POD 类型:不会初始化(值未定义)
TChunkedArray<int32> IntArray;
// new FChunk 时:4096 个 int32 不会被初始化,速度快
// 非 POD 类型:调用每个元素的默认构造函数
TChunkedArray<FString> StringArray;
// new FChunk 时:每个 FString 都会调用构造函数,速度慢!
性能陷阱示例
cpp
与 TArray 的对比
| 容器 | 添加元素时的构造行为 |
|---|---|
TArray |
只构造实际添加的元素 |
TChunkedArray |
构造整个 Chunk 的所有元素(非 POD 时) |
最佳实践
cpp
方案 2 详解:TOptional 延迟构造原理
TOptional 的本质:
cpp
内存对比:
性能对比(假设每个 Chunk 256 个元素):
cpp
// 直接存储
TChunkedArray<FExpensiveType> Array;
Array.Add(1);
// 代价:256 次 FExpensiveType() 构造,256 次内存分配
// TOptional 存储
TChunkedArray<TOptional<FExpensiveType>> Array;
Array.Add(1);
Array[0].Emplace();
// 代价:256 次 TOptional()(几乎没有成本)+ 1 次 FExpensiveType() 构造
使用方式:
cpp
| 方式 | new FChunk 时 | 实际使用时 |
|---|---|---|
| 直接存储 | 构造所有元素(慢) | 直接使用 |
| TOptional | 只分配内存(快) | 需要 Emplace + IsSet 检查 |
总结:延迟构造 = 把构造成本从"创建 Chunk 时"推迟到"实际使用时"
4. 内存布局详解
4.1 整体结构
4.2 索引映射
cpp
// 假设 NumElementsPerChunk = 4096,访问 Element[10000]
ChunkIndex = 10000 / 4096 = 2
ChunkOffset = 10000 % 4096 = 1808
return Chunks[2].Elements[1808];
5. 核心操作实现
5.1 元素访问
cpp
ElementType& operator[](int32 ElementIndex)
{
const uint32 ChunkIndex = ElementIndex / NumElementsPerChunk;
const uint32 ChunkElementIndex = ElementIndex % NumElementsPerChunk;
return Chunks[ChunkIndex].Elements[ChunkElementIndex];
}
5.2 添加元素
cpp
5.3 Placement New 支持
cpp
5.4 其他重要方法
cpp
5.5 CopyToLinearArray 详解:为什么只支持 POD 类型
实现原理
cpp
为什么使用 memcpy
| 类型 | 正确的拷贝方式 | memcpy 是否安全 |
|---|---|---|
| POD | memcpy 或 = |
✅ 安全 |
| 非 POD | 必须调用拷贝构造/赋值 | ❌ 不安全 |
非 POD 使用 memcpy 的危险
cpp
内存示意图:
memcpy 前:
A.Data.Ptr ──→ [1, 2, 3]
B.Data.Ptr ──→ nullptr
memcpy 后(浅拷贝):
A.Data.Ptr ──→ [1, 2, 3] ←── B.Data.Ptr // 两个指针指向同一内存!
析构时:
~B() → delete [1, 2, 3] // 释放内存
~A() → delete ??? // 野指针,崩溃!
POD 类型为什么安全
cpp
性能优势
cpp
// POD 类型:memcpy 一次性拷贝整个 Chunk(极快)
// 假设 Chunk 有 4096 个 int32
FMemory::Memcpy(Dest, Chunk.Elements, 4096 * 4); // 一次调用,拷贝 16KB
// 非 POD 类型:必须逐个调用拷贝构造(慢)
for (int32 i = 0; i < 4096; ++i)
{
new(&Dest[i]) ElementType(Chunk.Elements[i]); // 4096 次函数调用
}
memcpy 比逐个拷贝快 10-100 倍,这也是 UE 选择只支持 POD 的原因。
非 POD 类型的替代方案
cpp
// 如果需要拷贝非 POD 类型,手动遍历
TChunkedArray<FString> ChunkedStrings;
TArray<FString> LinearStrings;
LinearStrings.Reserve(ChunkedStrings.Num());
for (const FString& Str : ChunkedStrings)
{
LinearStrings.Add(Str); // 正确调用拷贝构造
}
6. 迭代器机制
6.1 迭代器结构
cpp
6.2 begin() 和 end() 实现
cpp
6.3 聚合初始化详解
迭代器使用 聚合初始化(Aggregate Initialization)而非构造函数:
cpp
// begin(): 只提供第一个成员,其余使用默认值
FIterType{Chunks.GetData()}
// 等价于:Chunk = Chunks.GetData(), Count = 0, ElementIndex = 0
// end(): 提供前两个成员
FIterType{nullptr, uint32(NumElements)}
// 等价于:Chunk = nullptr, Count = NumElements, ElementIndex = 0
聚合初始化的工作方式:
cpp
struct Aggregate
{
int A;
int B = 10; // 默认成员初始化器
int C = 20;
};
Aggregate x{1}; // A=1, B=10, C=20(未提供的使用默认值)
Aggregate y{1, 2}; // A=1, B=2, C=20
Aggregate z{1, 2, 3}; // A=1, B=2, C=3
聚合类型的条件:
- 无用户声明的构造函数
- 无私有/保护的非静态成员
- 无虚函数
- 无虚基类
6.4 范围 for 循环的展开
当你写:
cpp
for (FEntity& Entity : Entities)
{
Entity.Update();
}
编译器展开为:
cpp
6.5 遍历过程图示
6.6 为什么 end() 的 Chunk 是 nullptr
cpp
FIterType end()
{
return FIterType{nullptr, uint32(NumElements)};
}
end() 返回的迭代器永远不会被解引用(*end() 是未定义行为),它只用于与 begin() 比较。比较时只检查 Count 是否相等,所以 Chunk 传什么都无所谓,传 nullptr 最简单。
6.7 使用方式
cpp
7. 高级特性
7.1 TOptional 集成
cpp
7.2 移动语义
cpp
TChunkedArray(TChunkedArray&& Other)
{
Move(*this, Other); // 只移动指针,不拷贝数据
}
// 使用
TChunkedArray<int32> Array1(10000);
TChunkedArray<int32> Array2 = MoveTemp(Array1); // 高效移动
8. 性能分析与对比
8.1 时间复杂度
| 操作 | TArray | TChunkedArray |
|---|---|---|
| 随机访问 | O(1) | O(1)* |
| 尾部添加(无扩容) | O(1) | O(1) |
| 尾部添加(需扩容) | O(n) | O(1) |
| 顺序遍历 | O(n) | O(n)* |
*TChunkedArray 有额外的计算开销
8.2 基准测试结果
cpp
结论:
- 添加性能(无预分配):TChunkedArray 显著更快
- 遍历/随机访问:TArray 略快
- 内存稳定性:TChunkedArray 更优
9. 使用场景与最佳实践
9.1 适合场景
cpp
9.2 不适合场景
cpp
9.3 最佳实践
cpp
10. 完整实战案例
10.1 游戏实体管理器
cpp
10.2 使用示例
cpp
11. 总结
11.1 核心优势
| 优势 | 说明 |
|---|---|
| 内存分配成功率高 | 不需要大块连续内存,碎片化环境下更可靠 |
| 扩容成本低 | O(1) 复杂度,只分配新 Chunk |
| 引用稳定性 | 添加元素不会使现有指针/引用失效 |
11.2 适用场景
- 大规模数据存储(万级以上)
- 长时间运行的服务
- 需要保持引用稳定性
- 网络复制系统
11.3 注意事项
- 不支持删除单个元素:只能
Empty()清空 - CopyToLinearArray 仅支持 POD 类型
- 随机访问略慢:有除法/取模计算开销
- 非线程安全:需要外部同步
11.4 源码位置
Engine/Source/Runtime/Core/Public/Containers/ChunkedArray.h

本文是原创文章,采用 CC BY-NC-SA 4.0 协议,完整转载请注明来自 布总
评论
隐私政策
0/500
滚动到此处加载评论...
