容器字符串与内存

自动关联目录:容器字符串与内存

这一页讲的是"每天写代码都在用、但代价不清楚"的那批类型:TArray、TMap、TSet、FString、FName、FText,以及它们背后的分配器。

一句话定位:UE 不用 STL 不是因为造轮子癖,而是因为需要统一的内存统计、可预测的分配行为,以及跨平台的确定性。代价是这套容器有自己的规则,照 STL 的直觉用会踩坑。

三种字符串

类型本质用途代价
FName全局字符串表里的索引标识、Key、Tag、资源名创建要查表(有锁),但比较与复制极快
FString动态字符数组(TArray<TCHAR>)拼接、编辑、IO每次操作可能分配
FText本地化文本(含命名空间与键)所有显示给玩家的文本构造与查表最贵

判断口诀:

  • 要显示 → FText
  • 要拼接/解析/打印日志 → FString
  • 要比较/当 Key/反复传递 → FName
FName N = FName(TEXT("Head"));          // 查表一次
FString S = N.ToString();               // 转回字符串,有分配
FText T = FText::FromString(S);         // 转本地化文本

在循环里做 FString → FName 或反过来,是隐藏的性能杀手。热路径上应该统一用 FName,只在边界转换一次。

常见误用

误用问题
UI 上用 FString无法本地化,且每帧重建
用 FString 做字典 Key比较是逐字符的,FName 只比索引
拼字符串用 + 循环每次都重新分配,用 FString::Printf 或 Append
FName 用 TEXT() 之外的变量频繁构造查表加锁,多线程下更明显
日志里格式化复杂字符串即使日志级别关闭也会先拼出来

容器

容器底层特点
TArray<T>连续内存默认首选;随机访问快,中间插入慢
TMap<K,V>哈希表查找 O(1),无序
TSet<T>哈希表去重集合
TSortedMap有序需要有序遍历时用
TQueue / TDeque队列/双端见 实现双端队列
TStaticArray / TInlineAllocator栈上/内联小数组避免堆分配

TArray 的关键行为

TArray<int32> A;
A.Reserve(1024);        // 预留容量,避免多次扩容
A.Add(1);
A.Emplace(2);           // 原地构造,少一次拷贝
A.SetNum(10);           // 改大小(可能构造/析构元素)
A.SetNumUninitialized(10);  // 不初始化,仅用于 POD,更快
A.Reset();              // 清空但保留已分配内存
A.Empty();              // 清空并释放内存
A.Shrink();             // 释放多余容量
操作复杂度注意
Add / Emplace均摊 O(1)扩容会整体搬移
中间 Insert / RemoveO(n)会搬移后续元素
RemoveAtSwapO(1)不保序,性能敏感且不在意顺序时用
SortO(n log n)用 TArray::Sort 而非 std::sort 以走 UE 分配器
FindO(n)频繁查找应换 TMap

Reserve 是被低估的一行代码:在明确知道数量级时预留,能消掉一整条 realloc 链。

TMap / TSet

TMap<FName, int32> Counts;
Counts.Reserve(256);
Counts.FindOrAdd(TEXT("Coin")) += 1;
if (int32* Ptr = Counts.Find(TEXT("Coin"))) { /* ... */ }
注意说明
自定义 Key 要实现 GetTypeHash否则编译失败
Find 返回指针没找到是 nullptr,不要先 Contains 再 Find(查两次)
遍历时删除用迭代器 RemoveCurrent,不要 Remove 后继续用旧索引
TMultiMap一键多值
for (auto It = Counts.CreateIterator(); It; ++It)
{
    if (It->Value <= 0) { It.RemoveCurrent(); }
}

智能指针

类型对象用途
TSharedPtr / TWeakPtr非 UObject引用计数,UE 自己的实现(带调试信息)
TUniquePtr非 UObject独占所有权
TWeakObjectPtrUObjectGC 安全弱引用(见 GC)
TSoftObjectPtrUObject 资产不强制加载
TStrongObjectPtrUObject强引用保活

UObject 不要放进 TSharedPtr——两套生命周期机制打架,会出现"引用计数认为活着但 GC 已经回收"的野指针。

分配器

分配器用途
FMemory::Malloc/Free/Realloc走引擎分配器,会计入内存统计
FMemory::Memcpy/Memzero平台优化版本
TInlineAllocator<N>容器小容量时内联在对象里,超过才堆分配
TFixedAllocator<N>固定容量,超出即崩
TMemoryImageAllocator免反序列化布局
FMallocAnsi / FMallocBinned2/3底层实现,-ansimalloc 可切换
// 小数组常驻在栈/内联区,避免堆分配
TArray<int32, TInlineAllocator<8>> SmallNums;

为什么不用 std::vector:UE 容器能被 FMemory 统计、能被内存分析工具(MemPro、LLM)识别、行为在各平台一致。混用 STL 的结果是内存报告对不上账。

内存统计与排查

命令作用
stat memory总览,各分类占用
memreport -full生成完整内存报告
LLM(Low Level Memory tracker)逐系统追踪,需启动时开启 -llm
stat streaming资源流式加载状态

排查路线:先 memreport 看是哪一类(Texture / Mesh / UObject / 自定义),再针对该类深入。具体工具见 Profile。

常见坑

坑说明
用 FString 做高频比较的 Key应换 FName
循环里 + 拼字符串多次分配,用 Append / Printf
忘记 Reserve大量小 realloc
遍历容器时按索引 Remove索引错位,漏删或越界
UObject 放进 TSharedPtr双重生命周期管理,野指针
混用 std:: 容器内存统计对不上,分析工具看不到
TArray 存大对象并按值传递触发深拷贝,用引用或 TArrayView
用 Contains + Find查两遍,直接 Find 判空即可
返回 TArray 而不保留容量反复分配;复用成员数组 + Reset