“Rainbow Six Siege: Quest for Performance” 这个标题看起来像是在谈论《彩虹六号:围攻》(Rainbow Six Siege)这款游戏为了追求更高性能所做的优化和技术探索。
理解要点:
ubiProfile()
和 ubiProfileEvent()
:void Foo()
{
ubiProfile(Foo); // 标记整个函数Foo的性能
for(int i = 0; i < 10; ++i) {
ubiProfileEvent(Processing); // 标记循环中每次处理事件的性能
}
}
printf()
一样用格式化字符串输出标签,比如:ubiProfileFormat("Processing asset: %s", assetName);
InplaceString<256>
示例:const char* Func()
{
InplaceString<256> str;
...
return StringFormat("%s", str.GetBuffer());
}
这段内容讲的是如何针对复杂且历史遗留的内存管理体系,通过无锁分配器和代码优化,提高内存分配效率,从而支持整体性能测试和优化工作。
void MyTask::Execute() {
Array<...> myArray; // array 在栈上,但 buffer 在堆上
populate(myArray);
process(myArray);
} // myArray 析构
Array
类可能在 Grow
或初始化时动态申请 buffer(实际分配在 heap 上)。void WorkerThread::ProcessTasks() {
TaskAllocator allocator(128KB); // 每个线程拥有自己的分配器(临时内存池)
while (1) {
Task* newTask = GetNextTask();
newTask->Execute();
allocator.Reset(); // 任务完成后重置分配器
}
}
void Array::Grow(std::size_t size) {
Allocator* allocator = DefaultAllocator;
if (IsOnStack(this))
allocator = g_TLSTaskAllocator; // 检查是否是临时任务
m_Buf = allocator->Realloc(m_Buf, size);
}
bool IsOnStack(const void* ptr) {
unsigned char stackEnd;
if (ptr >= (&stackEnd + (1<<20)) || ptr <= &stackEnd)
return false; // 如果超出栈的合理范围
return ptr < m_StackStart; // 每个线程有自己的栈起点
}
thread_local
保存栈起点 m_StackStart
这个 Task Allocator 模式极其高效,是优化临时对象生命周期、减少内存分配开销的利器。在任务调度系统、游戏引擎、图形处理中非常实用。重点是:
Array
的使用方式进行分析、记录和优化。下面是对各部分内容的详细理解和归纳:频繁的小块内存分配是性能的天敌,尤其是在需要实时响应的系统中(如游戏引擎)。所以通过自动工具(如 ArrayAnalyzer)来监控、分析数组使用,找出优化点是非常关键的。
Array<float> x;
x.Reserve(1024);
for (...) {
x.Add(...);
}
InplaceArray
):InplaceArray<float, 8> x;
x.Reserve(...);
for (...) {
x.Add(...);
}
InplaceArray
会先尝试使用栈上的缓冲区(最多存 N
个元素),N
后再转向堆分配,从而减少堆分配次数。Array(..., [CallerFilePath], [CallerLineNumber]);
CallerFilePath
,C++ 实现使用 _ReturnAddress()
:void* addr = _ReturnAddress();
#define ubiRegisterArrayStats(type) \
GetArrayStats().Init(_ReturnAddress(), \
sizeof(Base::ValueType), \
ArrayStats::ArrayType::type, \
typeid(Base::ValueType).name())
~Array() {
#ifdef UBI_ARRAY_STATS
if (m_IsMemoryOwner)
ArrayStats::RegisterStats(m_ArrayStats);
#endif
}
void Foo() {
Array<ubiVector4> someVec4Array;
Array<float> someFloatArray;
}
class MyClass {
Array<float> m_SomeMember;
};
这些都会被自动记录,供后续分析是否该优化为 InplaceArray
或增加 Reserve
。
InplaceArray
替代纯堆数组。ArrayAnalyzer 是一个非常实用的优化辅助工具,它通过自动化统计 Array
的构造使用位置、使用方式、类型等,帮助开发者识别哪里可能存在多次内存分配,进而引导开发者优化:
Reserve
合理预分配InplaceArray
利用栈空间ArrayAnalyzer
宏或 InplaceArray
的实现示例代码吗?还是你想了解如何接入这种分析到你自己的项目中?std::function
的优化提案),也涵盖了锁优化方面的设计(Lock-Free
)。以下是逐段详解和整体理解:频繁的内存分配/释放不仅浪费 CPU 时间,而且容易产生碎片、影响缓存效率、导致性能抖动。你收集的内容展示了一系列解决方案来统计、分析、优化内存分配路径,尤其是针对数组和函数对象(std::function
)的使用。
这是一个用于分析数组分配行为的工具系统,可以识别哪些数组构造点可能存在优化空间。
void* addr = _ReturnAddress(); // 在构造函数中记录调用方地址
类似 C# 的 [CallerFilePath]
和 [CallerLineNumber]
。typeid(Base::ValueType).name() // 获取元素类型
#define ubiRegisterArrayStats(type) ...
~Array() {
if (m_IsMemoryOwner)
ArrayStats::RegisterStats(m_ArrayStats);
}
Array
替换为 InplaceArray
。Reserve()
以避免多次增长。标准 std::function
在闭包捕获多一点数据时就会退化成堆分配,导致性能开销:
std::function<void()> f1 = [&]{ foo(x); }; // OK,无堆分配
std::function<void()> f2 = [&]{ foo(x, y); }; // 会触发堆分配
std::inplace_function
提案目标是限定闭包大小,避免堆分配:
std::inplace_function<void(), 64> f = [&]{ foo(x, y); };
开源实现:
https://github.com/WG21-SG14/SG14/blob/master/SG14/inplace_function.h
该提案由你提到的 Carl Cook 共同参与推动。
这部分关注于锁使用优化,降低线程竞争带来的性能瓶颈。
UbiAdaptiveMutex m_Lock;
void SomeFunc() {
ubiAutoLock(m_Lock);
...
}
虽然不是完全 lock-free,但名字中的 “Adaptive” 暗示:
std::atomic
、lock-free 队列等)。技术点 | 优势 |
---|---|
ArrayAnalyzer |
自动分析数组分配点,减少不必要堆分配 |
InplaceArray |
利用栈内存避免 heap 分配 |
std::inplace_function |
减少 std::function 的隐式堆分配 |
锁分析 (LockAnalyzer ) |
识别热点锁,推动替代方案或优化临界区 |
优化工具宏 | 简单封装宏,初级开发者也能参与优化 |
如果你希望: |
Array
使用建议的工具,inplace_function
或用现有替代品,LockFreeQueue
代码结构的完整重构版本和逐步分析。这是一个 无锁队列(Lock-Free Queue) 实现,旨在 高性能并发场景(如游戏引擎、任务系统等)中使用。当然可以!下面是加了详细注释版的 LockFreeQueue
相关代码,包括 Enqueue
、Dequeue
以及 DequeueSingleThreadScope
,并解释每个字段和步骤的作用,方便你理解底层原理与设计。
// 一个通用的、可嵌入策略的无锁环形队列
template<
typename T, // 队列中元素的类型
std::uint32_t SizeT, // 队列大小(必须为2的幂,便于取模优化)
typename TypeTraitT = LockFreeQueueDefaultTrait<T>, // 类型特性(比如空值定义)
typename DataPolicyT = LockFreeQueueEmbeddedDataPolicy<T, SizeT> // 数据存储策略
>
class LockFreeQueue {
public:
// 定义自身类型方便使用
typedef LockFreeQueue<T, SizeT, TypeTraitT, DataPolicyT> Type;
// 队列可容纳的最大元素数(保留部分空间防止读写交错)
enum {
MAX_SIZE = (SizeT - TypeTraitT::MAX_THREAD_COUNT)
};
/// 向队列中添加一个元素
void Enqueue(T value) {
// 检查 SizeT 是否满足取模优化要求(SizeT 必须是 2^n)
static_assert((0xFFFFFFFF % SizeT) == (SizeT - 1),
"(U32 max + 1) must be a multiple of SizeT");
// 获取写入索引(使用原子加,并且做环形缓冲区取模)
std::uint32_t index = m_QueueWritePos++ % SizeT;
// 将值写入缓存中
m_Elements[index] = value;
// 增加元素计数器
std::int32_t count = ++m_QueueCount;
// 断言检测溢出(防止超过允许的最大并发大小)
ubiAssert(count <= MAX_SIZE);
}
/// 从队列中移除并返回一个元素
T Dequeue() {
// 尝试减少队列元素数量
std::int32_t count = --m_QueueCount;
// 如果计数变成负数,表示队列为空,需要回滚
if (count < 0) {
++m_QueueCount; // 回滚操作
return TypeTraitT::GetNull(); // 返回预设的空值
}
// 获取读取索引
std::uint32_t index = m_QueueReadPos++ % SizeT;
// 返回取出的值
return m_Elements[index];
}
private:
DataPolicyT m_Elements; // 环形缓冲区,存储元素
std::atomic<std::uint32_t> m_QueueReadPos{0}; // 当前读取位置(原子操作)
std::atomic<std::uint32_t> m_QueueWritePos{0}; // 当前写入位置(原子操作)
std::atomic<std::int32_t> m_QueueCount{0}; // 当前队列元素个数(原子操作)
};
DequeueSingleThreadScope
这个类支持批量处理队列中的所有元素,适合在单线程中快速拉取所有任务并处理。
class DequeueSingleThreadScope {
public:
// 构造函数:对队列做快照(读取位置 + 元素数量)
DequeueSingleThreadScope(Type& queue)
: m_Queue(queue)
{
// 快照当前的读取位置与元素数量
m_QueueCountSnapshot = queue.m_QueueCount.load();
m_QueueReadPosSnapshot = queue.m_QueueReadPos.load();
}
// 可选构造函数:指定要读取多少元素
DequeueSingleThreadScope(Type& queue, std::int32_t dequeueCount)
: m_Queue(queue),
m_QueueCountSnapshot(dequeueCount),
m_QueueReadPosSnapshot(queue.m_QueueReadPos.load()) { }
~DequeueSingleThreadScope() {
// 作用域结束后,不做清理,因为它只是快照
}
// 内部定义的迭代器,支持范围 for 使用
class Iterator {
public:
Iterator(Type& q, std::uint32_t pos) : m_Queue(q), m_Index(pos) {}
T& operator*() {
return m_Queue.m_Elements[m_Index];
}
Iterator& operator++() {
m_Index = (m_Index + 1) % SizeT;
return *this;
}
bool operator!=(const Iterator& other) const {
return m_Index != other.m_Index;
}
private:
Type& m_Queue;
std::uint32_t m_Index;
};
// 返回迭代器起始位置
Iterator begin() {
return Iterator(m_Queue, m_QueueReadPosSnapshot % SizeT);
}
// 返回迭代器结束位置
Iterator end() {
return Iterator(m_Queue,
(m_QueueReadPosSnapshot + m_QueueCountSnapshot) % SizeT);
}
private:
Type& m_Queue; // 引用目标队列
std::int32_t m_QueueCountSnapshot; // 当前队列元素数量(快照)
std::uint32_t m_QueueReadPosSnapshot; // 当前读取位置(快照)
};
优势 | 描述 |
---|---|
高性能 | 原子操作实现读写,不依赖锁,适合高并发环境 |
环形缓存 | 使用固定内存空间,避免频繁堆分配 |
范围读取 | DequeueSingleThreadScope 支持 for(auto& e : ...) 批处理 |
安全控制 | 使用原子变量和回滚逻辑,避免多线程冲突数据错误 |
可扩展性 | 支持自定义数据存储策略和类型特征 |
LockFreePool
的 NextFreeInfo
结构体代码 的整理与理解分析。struct NextFreeInfo {
// 构造函数:显式传入两个字段
NextFreeInfo(std::uint32_t nextFreeIndex, std::uint32_t versionCounter)
: m_NextFreeIndex(nextFreeIndex), m_VersionCounter(versionCounter) {}
// 构造函数:从 64 位原子值解构
NextFreeInfo(std::uint64_t nextFreeAtomic)
: m_NextFreeAtomic(nextFreeAtomic) {}
union {
struct {
std::uint32_t m_NextFreeIndex; // 下一个空闲元素的索引
std::uint32_t m_VersionCounter; // ABA 问题解决方案
};
std::atomic<std::uint64_t> m_NextFreeAtomic; // 原子联合体
};
};
union
用于两个视角的访问m_NextFreeIndex
是当前节点指向的下一个空闲槽位的索引m_VersionCounter
是一个 版本号,用于避免 ABA 问题m_NextFreeAtomic
把这两个字段作为一个 64 位值,以原子方式一次性读写在 lock-free 结构中,如果一个值从 A 变成 B 再变回 A,compare_exchange
可能会误以为值没有变过。
版本号用于识别这种情况,即使值恢复到了 A,版本号也不同,从而避免误判。
此结构很常见于 lock-free 内存池或 freelist:
// Enqueue 示例(伪代码)
NextFreeInfo oldHead = head.load();
NextFreeInfo newHead(index, oldHead.m_VersionCounter + 1);
head.compare_exchange_weak(oldHead, newHead);
通过:
m_NextFreeIndex
:记录下一个节点的位置(链表)m_VersionCounter
:防止 CAS 的 ABA 问题字段 | 含义 |
---|---|
m_NextFreeIndex |
指向下一个空闲元素的索引(链式结构) |
m_VersionCounter |
版本计数器,防止 ABA 问题 |
m_NextFreeAtomic |
原子操作接口,组合上述两个字段 |
union 的使用 |
支持结构访问与原子访问的共享内存 |
LockFreePool
(无锁内存池)和它的图示结构,展示的是 Lock-Free(无锁)对象池的内存布局与管理策略。下面我帮你详细解析并理解这些结构。LockFreePool
图示结构你给出的多个「图示」类似于这样:
LockFreePool
├── Node
│ └── Object
├── Node
│ └── Object
...
或者:
LockFreePool
├── Object
├── Object
├── Node
│ └── Object
概念 | 含义 |
---|---|
Object | 实际使用的对象,如一个 MyStruct 、EnemyEntity 等 |
Node | 对象包装器,额外携带元数据(如下一个空闲索引) |
LockFreePool | 无锁内存池,用于多线程高效复用对象,避免频繁分配释放 |
struct Node {
NextFreeInfo m_FreeInfo; // 链接信息(索引 + 版本号)
T m_Object; // 实际的用户数据对象
};
m_FreeInfo
是我们前面提到的:
m_NextFreeIndex
: 下一个空闲块的索引m_VersionCounter
: 版本号用于防止 ABA 问题Node
构成一个 栈式的空闲列表链std::vector<Node> m_PoolBuffer; // 存储所有对象和元数据
std::atomic<uint64_t> m_FreeListHead; // 管理空闲链表头,原子操作
// 原子 pop 一个空闲节点
NextFreeInfo oldHead = m_FreeListHead.load();
Node& node = m_PoolBuffer[oldHead.m_NextFreeIndex];
// 返回 node.m_Object 指针
// 把释放的 node 推回空闲链表头
NextFreeInfo newHead(index, oldHead.m_VersionCounter + 1);
m_FreeListHead.compare_exchange_weak(oldHead, newHead);
你看到的图示里,Node
和 Object
被分开或混合展示,是为了说明两种角度:
结构图 | 含义 |
---|---|
Node → Object |
正常结构:每个 Node 持有一个 Object 和元信息 |
Object Object Node Node Object |
表示对象顺序分配中穿插空闲链 |
这些表示是为了说明:对象池中的数据和空闲信息交错,构成一种优化结构,支持 lock-free 分配释放。 |
LockFreePool
的关键方法 CreateWithoutConstructor
和 DestroyWithoutDestructor
,这两个函数是无锁池的分配和回收核心。CreateWithoutConstructor
T* CreateWithoutConstructor() {
while (true) {
// 获取当前空闲链表头(原子读取64位)
NextFreeInfo currentFreeInfo(m_NextFreeInfo.m_NextFreeAtomic);
std::uint32_t nextFreeIndex = currentFreeInfo.m_NextFreeIndex;
// 判断索引是否合法,没有空闲节点则返回nullptr
if (!m_DataPolicy.IsValidObjectIndex(nextFreeIndex))
return nullptr; // pool is full
// 获取下一个空闲节点索引(链表下一节点)
std::uint32_t nextNextFreeIndex =
m_DataPolicy.GetNodeFromIndexCanExpand(nextFreeIndex)->m_NextFreeIndex;
// 构造新的空闲链表头:指向 nextNextFreeIndex,版本号+1
NextFreeInfo newNextFreeInfo(nextNextFreeIndex, currentFreeInfo.m_VersionCounter + 1);
// CAS操作:尝试将空闲链表头从 currentFreeInfo 替换为 newNextFreeInfo
if (m_NextFreeInfo.CompareExchange(currentFreeInfo, newNextFreeInfo)) {
// CAS成功,分配成功,返回对应对象节点指针
Node* node = (Node*)m_DataPolicy.GetNodeFromIndex(nextFreeIndex);
return (T*)node;
}
// CAS失败,循环重试
}
}
m_NextFreeInfo
代表空闲链表头,存的是 {当前空闲节点索引, 版本号}
。DestroyWithoutDestructor
void DestroyWithoutDestructor(T* object) {
Node* objectAsNode = (Node*)object;
Node* newHead = objectAsNode;
std::uint32_t newNextFreeIndex = m_DataPolicy.GetIndexFromNode(newHead);
while (true) {
// 读当前空闲链表头
NextFreeInfo currentFreeInfo(m_NextFreeInfo.m_NextFreeAtomic);
std::uint32_t currentFreeIndex = currentFreeInfo.m_NextFreeIndex;
// 构造新的头节点 info
NextFreeInfo newNextFreeInfo(newNextFreeIndex, currentFreeInfo.m_VersionCounter + 1);
// 新节点指向旧链表头,实现压栈
newHead->m_NextFreeIndex = currentFreeIndex;
// CAS 更新空闲链表头
if (m_NextFreeInfo.CompareExchange(currentFreeInfo, newNextFreeInfo)) {
return; // 成功回收
}
// 失败重试
}
}
方法名 | 作用 | 关键点 |
---|---|---|
CreateWithoutConstructor |
无锁分配一个节点 | 原子弹栈,CAS 更新空闲链表头 |
DestroyWithoutDestructor |
无锁回收一个节点 | 原子压栈,CAS 更新空闲链表头 |
NextFreeInfo
保存链表节点索引和版本号,防止 ABA 问题。m_DataPolicy
负责索引与节点的映射及合法性判断。RAM (最慢)
↓
L3 Cache
↓
L2 Cache
↓
L1 Cache (最快)
↓
Core
PageProtectAllocator
作为一种数据策略应用到LockFreePool
中。#if defined(UBI_FINAL) || defined(UBI_PROFILE)
#define DefaultDataPolicy FixedDataPolicy<T, SizeT>
#elif defined(UBI_EDITOR)
#define DefaultDataPolicy MultiDataPolicy<T, SizeT, 100, 10>
#else
#define DefaultDataPolicy MultiDataPolicy<T, SizeT, 10, 1>
#endif
template <typename T, size_t SizeT, typename DataPolicyT = DefaultDataPolicy>
class LockFreePool { ... };