内存布局与分配
全库目录221
本文目录15
设一帧更新一批粒子,CPU计算位置和速度,随后复制队列把结果送给GPU。我们先决定热字段怎样连续读取,再为这批数据分配对齐存储、构造辅助对象,最后等待CPU与GPU消费者退出后复用帧槽。总字节数相同并不意味着访问成本相同;CPU帧结束也不意味着这一批存储已经能重用。
数据局部性
这批先取4个粒子。更新读取位置和速度,绘制准备还读取年龄;我们先为三个字段确定明确的教学布局,再决定如何分配。CPU缓存按缓存行搬运数据,访问布局因此先由这些循环决定。连续遍历能让同一缓存行中的多个字段被实际使用,随机追逐对象指针则可能每次只用一行中的少数字节。
结构数组:
[pos vel age][pos vel age][pos vel age]分量数组:
[pos pos pos][vel vel vel][age age age]如果更新只读取位置/速度,SoA能减少无关字段进入缓存,也便于SIMD批处理。绘制准备同时需要年龄时,又要读取另一条数据流;布局收益因此由各阶段实际读取的字段共同决定。例如每个对象64字节、热循环只读8字节时,集中热字段可以减少无效传输,但所有字段都拆开也会增加索引与多流维护。分别测粒子更新和完整帧,才能判断局部收益是否保留到最终结果。
字段大小与上传布局
本例把位置和速度各存为一个16字节四分量浮点记录,年龄为4字节浮点数。规定AoS记录起点和步长均满足16字节对齐,字段偏移依次为0、16、32,末尾补12字节,步长为48字节;4个粒子占192字节。这里的布局由接口明确规定,CPU与GPU逐字段核对,不依赖某个编译器恰好生成相同结构。
SoA分别分配位置64字节、速度64字节和年龄16字节,共144字节有效存储。数组起点另满足各自对齐,分配间的填充计入实际占用。教学缓存行取64字节且数组起点按缓存行对齐时,更新只需位置和速度的两行;同一批AoS覆盖三行。这个数例只说明本次字段读取的足迹,预取、写分配和真实缓存层次还会改变传输量。
GPU若接受相同SoA,可直接上传三份区间。这里选择GPU消费48字节AoS,CPU在上传准备阶段把三份数组打包到192字节缓冲:第i项写入偏移48*i的位置、速度和年龄,并初始化填充字节。省下的CPU热循环足迹因此带来一次打包及额外存储,最终取舍要把更新、打包、上传和绘制一起测量。后文沿这条CPU SoA到GPU AoS的路线安排分配与退休。
缓存映射与写策略
直接映射缓存中每个内存块只能进入一个缓存组,硬件简单但容易发生冲突抖动。组相联缓存允许一个组内有多个路,是容量、命中率和查找成本的常见平衡。全相联缓存允许放入任意位置,冲突少但查找和替换成本高,通常只用于较小结构。
一个组满时需要替换策略,例如 LRU、近似 LRU 或随机替换。直写在写缓存时同步更新下层内存,行为直接但写流量大;写回先标记脏标记,驱逐时才回写,减少流量但一致性和状态管理更复杂。游戏代码通常不能选择硬件策略,但可以通过连续访问、减少工作集和避免多个热数组落入同一冲突模式来提高命中率。
设教学缓存有4组、每行64字节,组号为floor(address/64)%4。地址0和256均进入组0,直接映射下交替读会反复互相驱逐,即使总共只用两行;两路组相联可同时保存它们,第三个同组热块才需要替换。真实缓存的索引、地址翻译与替换策略更复杂,这个模型仅解释冲突失效,不能由数组间距直接预测目标CPU耗时。
栈与堆
布局已经给出了144字节源数组和192字节上传区。更新函数的局部计数器可以随调用结束,但这两批字节还要交给后续任务。线程栈保存调用帧、返回地址和局部数据。自动存储期对象在所属块退出时结束寿命,实际数据可在寄存器或调用帧中;栈指针与槽位复用由ABI和编译器安排,不能把每次块退出等同于一次栈指针回退。线程栈容量有限。
堆支持动态大小和跨作用域生命周期。分配器要维护空闲块、对齐和并发,频繁小分配可能造成开销与碎片。
语言类型并不单独决定这份数组在哪儿。“值类型在栈、引用类型在堆”只是 C# 入门近似。值类型可以嵌在堆对象、数组或被装箱;JIT/AOT 也可能优化。真正要看对象布局和逃逸/运行时行为。
虚拟地址与物理存储
为批次保留的地址与实际驻留页又是两层问题。进程看到虚拟地址空间,操作系统按页映射物理内存。提交大块地址不一定立刻占用全部物理页,但首次访问、缺页异常和换页会影响性能。
这批粒子的GPU资源可能位于独立VRAM或统一内存。上传堆、读回堆、暂存缓冲与设备本地资源有不同访问成本;选择CPU分配器之前,先确定CPU结果是直接供GPU读取,还是复制到另一资源。
移动SoC的CPU与GPU共享物理内存,不等于拷贝和带宽免费。缓存一致性、片上瓦片内存、总线和资源同步仍然存在。
分配器与碎片
热字段布局确定以后,还要决定这一批存储怎样取得和归还。同一帧一起退休的粒子临时数据可使用线性分配区,跨帧保存的资产则需要不同寿命的分配器。外部碎片指空闲总量足够、却没有足够大的连续块;内部碎片指分配块因对齐或尺寸类别向上取整,实际占用大于请求值。
常见策略:
- 线性分配器:整批分配、整批释放;
- 对象池/Slab:固定尺寸类别,快速复用;
- 栈分配器:后进先出;
- Buddy 分配器:按二次幂拆分合并;
- 通用堆:灵活但元数据和碎片更复杂。
这批源数组、上传区和辅助对象一起进入帧分配区。源数组即使已经打包完,也可随整批统一退休,代价是多保留一段内存;需要更早回收时为它建立独立寿命。跨帧资产若混入同一区,会使整个区必须等资产结束才能重置。
以64字节空闲区为例,先分配两个16字节块,再释放第一个;若后32字节仍空闲,总空闲48字节却可能无法容纳一个40字节块,这就是外部碎片。固定32字节尺寸类服务20字节请求,则每块有12字节内部浪费。池减少查找和碎片变化,但不能消除尺寸向上取整。
Buddy可以从64拆成两个32,再把其中一个拆成两个16;释放一个16时,只有同阶伙伴也空闲才能合并成32。仅有另一个不相邻的16空闲,不能组成所需32。栈分配器则记录分配前游标,只允许逆序回退;释放下层块会覆盖仍活跃的上层块。具体固定槽位复用见 对象池。
Buddy的合并也依赖精确身份。以相对分配区基址的偏移o和块大小b表示块,同阶伙伴偏移为o XOR b。只有伙伴在同一分配区、同阶空闲表中确实空闲,才移除两块,以较小偏移和2b继续合并;“旁边有足够空闲字节”不能替代伙伴判断。分配先把请求加上头部与对齐开销,再向上取到可支持的阶,元数据记录实际阶,释放时才能返回正确空闲表。
帧分配器
这里为粒子上传数据和本帧辅助对象采用线性分配器。伪代码输入非负大小、游标与容量,使用固定宽度无符号整数;MAX为该整数最大值。基址满足本次对齐要求,对齐为可表示的正二次幂,且容量不超过MAX。输出当前分配区中的独占区间,整批使用结束以后才能重置。
if cursor > capacity: fail
if cursor > MAX - (alignment - 1): fail
offset = (cursor + alignment - 1) & ~(alignment - 1)
if offset > capacity: fail
if size > capacity - offset: fail
cursor = offset + size
return base + offset线性游标的计算要求独占分配入口。多个任务共享分配区时,读取游标、计算对齐、验证容量和写回新游标都在同一锁内执行。只把最后写回改成原子操作,两个任务仍可能领取同一范围。无锁实现以旧游标为比较值做CAS,失败后重新计算对齐与容量。高并发时也可给工作线程独立子区,整批复用仍等待全部子区消费者。
对齐与批次寿命
本例位置、速度数组和上传记录约定16字节对齐。前面的位运算以基址已经满足对齐为前提;若外部给出的基址只有较低对齐,单独对齐偏移仍会返回错位地址。分配区因此在创建时取得满足最大请求对齐的基址,超过该对齐的请求交给单独的过对齐分配路径。
例如游标13、对齐16、请求20字节,返回区间[16,36),消耗3字节对齐空隙。零大小请求可以返回当前对齐位置,但不产生可解引用的独占字节;若调用者需要唯一对象身份,应使用独立句柄或分配至少一个字节,不能把零长度返回地址当作唯一ID。
假设本批开始游标为13,位置64字节、速度64字节和年龄16字节分别请求64字节对齐,上传区192字节请求16字节对齐。得到的区间依次为[64,128)、[128,192)、[192,208)和[208,400),游标最终为400;初始47字节填充也属于本批占用。这个安排满足前文源数组按缓存行对齐的教学条件,上传区则满足GPU记录的16字节对齐。有效源数据144字节加上传192字节并不等于最终游标,分配器还要计算起点与空隙。CPU任务持有这些区间到打包结束,上传区继续供复制队列消费。
构造登记与提交次序
参见帧分配器的构造与退休。
析构依赖与变更检查
参见帧分配器的构造与退休。
异步批次与复用
参见帧分配器的构造与退休。
验证方法
统计分配数、峰值、碎片和访问事务。以不同访问顺序比较耗时,arena重置前确保异步读者结束;对齐与加法检查溢出。OS页表机制以虚拟内存为前置。
实现与证据
分配器正文为寿命伪代码。整数模型通过5项边界检查,覆盖精确占满、对齐后越界、对齐加法溢出、请求大小回绕风险和非法对齐;运行 python 知识库/examples/revision_16_21/test_review_reference.py 可复现。未实现平台分配器或测量缓存命中,性能结论保留工作负载条件。
相关主题
参考资料
-
C++工作草案:自动存储期:块作用域变量的存储期与销毁规则。
-
C++标准草案 except.ctor:2026-10-23核读构造异常期间已完成子对象的析构规则;帧分配区的提交次序由本页协议单独定义。
-
C++ Core Guidelines:R资源管理与Per性能规则。
-
Microsoft VirtualAlloc:MEM_RESERVE、MEM_COMMIT及按页分配。