← ALL ARTICLES

内存布局与分配

全库目录221
本文目录15

设一帧更新一批粒子,CPU计算位置和速度,随后复制队列把结果送给GPU。我们先决定热字段怎样连续读取,再为这批数据分配对齐存储、构造辅助对象,最后等待CPU与GPU消费者退出后复用帧槽。总字节数相同并不意味着访问成本相同;CPU帧结束也不意味着这一批存储已经能重用。

数据局部性

这批先取4个粒子。更新读取位置和速度,绘制准备还读取年龄;我们先为三个字段确定明确的教学布局,再决定如何分配。CPU缓存按缓存行搬运数据,访问布局因此先由这些循环决定。连续遍历能让同一缓存行中的多个字段被实际使用,随机追逐对象指针则可能每次只用一行中的少数字节。

结构数组:

text
[pos vel age][pos vel age][pos vel age]

分量数组:

text
[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。输出当前分配区中的独占区间,整批使用结束以后才能重置。

text
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 可复现。未实现平台分配器或测量缓存命中,性能结论保留工作负载条件。

相关主题

参考资料