性能体系:关键路径、工作集、预算与可验证的优化
这是跨模块综合分析。源码事实引用对应专题和本章证据表;公式是分析模型,实验与指标是新项目建议,均非实测结果。未编译、运行或基准测试工程。入口:Summary。
1. RAGE 性能设计的主轴#
RAGE 的性能特点可以概括为:离线把内容转为紧凑布局,运行时维持有限活动集合,用任务重叠可并行计算,再在明确阶段交付结果。它不是单靠某个快速 allocator、SIMD 内核或大线程池支撑开放世界。
最值得学习的是系统性的成本转移:
| 手段 | 移走的成本 | 新增的约束 |
|---|---|---|
| 资源内存镜像、布局匹配 | 每次装载解析与细分分配 | 版本、对齐、指针 fixup、工具正确性 |
| 定义/实例分离 | 重复内容存储与构建 | 实例和共享资源各自生命周期 |
| SoA、量化、紧凑索引 | cache miss 和带宽 | 编码上限、误差、数据更新一致性 |
| 异步 IO/计算/渲染 | 主线程串行阻塞 | 排队、发布、延迟回收和峰值内存 |
| LOD、sleep、虚拟 voice | 无意义的持续全量工作 | 质量损失、唤醒成本和恢复状态 |
| 固定池、scratch、有界队列 | 热路径分配与不可预测增长 | 峰值失败与降级策略 |
优化往往把问题从一个阶段推到另一个阶段。报告中任何“更快”都应理解为待验证的机制收益,而不是对当前硬件的百分比承诺。
2. 必须分清四种预算#
2.1 容量预算#
例如任务句柄池、dependency ring、composer nodes、物理 manifold pool、loader slots、drawlist pages。它们限制同一时刻可存在的工作或数据,不是每秒吞吐或整帧时间保证。
2.2 服务预算#
例如 streaming conversion 时间片、每帧处理批次、audio voice cost、网络目标字节。它们限制一个阶段愿意投入多少资源,不必然能限制一个不可分割工作的最长耗时。
2.3 截止时间#
pose 必须在物理或绘制消费前就绪,碰撞资源必须在高速移动物体到达前可用,音频块必须在设备消耗前供应。截止时间取决于消费者,不能只用统一的“后台低优先级”表达。
2.4 观测预算#
PF_*_BUDGETED 的 budget 是 timebar 标记参数;脚本 debug warning、profiler 阈值也不等于执行抢占。把告警线当成硬预算,会误判系统已经具备过载保护。
3. 帧时间由关键路径决定#
对一张已知依赖图,理想计算完成时间至少为:
T ≥ max(W / P_effective, L)
其中 W 是总 CPU 工作量,P_effective 是实际可用并行度,L 是最长依赖链。调度、锁、cache、外部设备和系统抢占只会进一步增加时间。这是下界模型,不是引擎自带计时公式。
图是依赖分析示例,实际阶段和交错由游戏注册/调用点决定。它说明:缩短一段不在关键路径上的后台任务可能没有 FPS 收益,但可能减少资源迟到;减少主线程收尾或延迟依赖可能比再加 worker 更有效。
流水线充分重叠时,长期帧吞吐常接近最慢阶段,而单次输入到呈现延迟包含多个阶段及缓冲等待。提高吞吐与降低输入延迟不能互相代替。render-done semaphore 只证明相应 CPU 渲染阶段完成,不能当作 GPU fence。
4. 任务粒度、拓扑与线程超额订阅#
4.1 更多任务不必更快#
若 N 个任务、每次调度成本 h,总计算 C,则简化开销为 N*h;每个任务太小会放大原子、句柄、event 和 cache 转移成本。批次过大则降低可分摊性,最后一个重任务决定完成时刻。
粒子按 emitter item 分批,而 emitter 的粒子量不同;物理按写冲突分 phase,而各 phase 工作量不同;动画按依赖图推进,而深链无法靠宽线程池拆开。三者需要不同分组策略,不能用一个全局 batch size 解决。
4.2 专用线程与 worker 争用同一组核心#
RAGE 同时有 dependency/task、render、audio、streaming、pathserver 等执行者。PC worker 数和 affinity 带有旧平台假设;应统计总 runnable 数、核利用率、迁核和实际排队,而不是只看某一个池的“空闲线程”。
混合大小核、多 CCD/NUMA 环境下,固定轮转亲和性可能破坏 cache 局部性或把 deadline 工作放在较慢核心。是否绑核需要测量;现代 CPU 核心多不是原配置天然更快的保证。
4.3 优先级不等于可推进性#
Dependency 四级共享 ring 高优先先取,回调失败可重排,未发现该段公平老化保证。提升动画优先级可能降低 pose 等待,却推迟为下一帧提供资源的低优先级工作。队列满自旋又会消耗消费者本应获得的 CPU。
建议追踪 ready width、queue age、执行时间、等待原因和 producer/consumer ID;同时让负载超过预算时有帮助执行、限流、拒绝或降级策略。此为新项目方向,不是声称现有调度器已具备全部机制。
5. 大世界是一条有状态服务流水线#
5.1 资源可用延迟的分解#
L_ready = 排队 + 依赖等待 + 读盘 + 解压/转换 + placement + 发布等待
各项可部分重叠;对于依赖链,应按实际依赖关键路径计算,不能简单把所有节点耗时全相加。硬盘换成 SSD 只直接改变其中一部分;若 placement 或主线程发布已成为瓶颈,IO 更快只会让完成队列堆积更快。
源码明确区分 loader slot、资源状态和异步完成标记,最终主线程 SetResource 后才发布 Loaded。应按这些状态采集时间,而非只统计 ReadFile 耗时。
5.2 预取距离必须覆盖端到端延迟#
简化直线运动模型:
D_prefetch ≥ v_peak * L_ready,p99 + D_margin
v_peak 是需要支持的移动速度,L_ready,p99 是端到端可用延迟的高分位,D_margin 包括预测误差、相机提前可见和资源使用范围。若资源还需物理或导航注册,相应时间也要计入。
转向、飞行、镜头切换和瞬移违反直线假设,需要额外兴趣点、显式预热或可见降级。世界显示距离增加会同时扩大地形/实体、纹理、导航和碰撞工作集,不能只调整 LOD distance 后期望一切线性变化。
5.3 稳态吞吐与突发缓冲#
设到达率 λ、服务率 μ,长期 λ 大于 μ 时任何有限队列都会耗尽。稳定条件下 Little 定律 平均在途数 = λ × 平均停留时间 可帮助解释为什么增加在途 slot 会增加内存,却未必减少延迟。
稳定场景下的 λ/μ 不足以覆盖瞬移和过场切换。需要记录短窗口突发字节、依赖扇出、placement 个数和回收滞后。loader 的 pending read 阈值和 conversion budget 是局部背压,不能自动组成整帧全局预算。
5.4 滞回和依赖闭包#
导航 Store 有装载/卸载距离差;世界 LOD 需要 loaded/attached 状态。它们都抑制边界来回抖动。模型卸载还需考虑贴图、动画、物理、脚本和绘制引用构成的依赖闭包。
过早回收导致重载,过晚回收增加驻留;单独追求缓存命中率会掩盖长期 pin。应同时观察 eviction 后短期重载比例、资源最后使用时间、引用来源以及实际可释放字节。
6. 内存峰值比稳态图表更危险#
分析上可按互斥类别记账:
M_peak = 常驻 + 活跃实例 + 在途输入 + 未发布输出 + 延迟回收 + 碎片/对齐 + 临时工作区
CPU/GPU 双份、defrag 新旧拷贝应归入对应项一次,避免重复计数。这是记账模型,不是某个 allocator 的原样字段。
| 峰值来源 | 容易误判的现象 | 应采集的信息 |
|---|---|---|
| 取消中的 IO/placement | 请求取消了,内存仍未下降 | cancel 与 completion 两个时间点 |
| render 引用和 GPU 使用 | 对象移除,纹理仍不可释放 | CPU 引用、提交帧、设备完成边界 |
| defrag 搬移 | 总空闲尚多却分配失败 | 最大可用块、pin、搬移临时双份 |
| 动画过渡 | 角色数不变但 scratch 暴涨 | 活跃图和中间 frame 峰值 |
| 碎裂/唤醒 | 平均物理对象很少仍爆池 | 单帧新增 collider/manifold/contact |
| 多兴趣点 | 玩家附近统计正常但驻留过大 | 每兴趣点边际资源与共享依赖 |
固定池没有消除内存问题,只把问题转换成容量和故障语义。开发版本的堆回退会掩盖发行版本上限;离线校验应根据发行约束执行。
Defrag 也不是免费恢复按钮:源码有 copy/place/finalise、多帧状态和 render flush,包含带宽、引用修复和等待。它适合被测量并控制频率,不应作为持续超预算内容的补救。
7. 数据布局的收益和代价#
场景 descriptor SoA、量化 BVH、physics bitsets、动画 block、cloth 八边批处理,都在减少有效工作所需读取的字节。优化顺序通常应先缩小活动集合,再让访问连续,最后才是单条算术指令。
对照简化 Roofline 思路:内核时间至少受 max(运算量/可用算力, 数据量/有效带宽) 限制,但随机依赖访问还可能受 latency 限制;不能只以理论内存带宽推导实际速度。
量化降低字节和 cache 占用,同时引入误差与 decode;SoA 提高批量读取效率,同时让单对象修改涉及多数组;预取只有在访问可预测且距离合适时有益。cache.h 的 prefetch 平台分支甚至可能为空实现,某条旧主机注释中的收益不能跨平台照搬。
共享原子计数器可能成为 coherence 热点,邻接字段也可能 false sharing。仅有 16-byte 对齐不保证 cache-line 隔离;布局应结合平台 cache line 以及实际读写所有权检查。
8. 物理优化:接触图比刚体数量更有解释力#
Broadphase 减少候选对,BVH 减少几何候选,manifold 维持接触,solver 消化约束。一个“有多少刚体”的数字不足以描述成本:稠密堆叠、关节链、自碰撞、composite children 和碎裂会改变 pair 数、窄相复杂度及并行冲突。
SplitPairs 按共享物体冲突安排并发 phase,scratch 限制 phase 数;同一堆物体的约束高度相连时,可并行宽度可能很小。增加核心并不改善约束传播收敛,减少 iteration 又可能增加穿透或关节误差。
CoreUpdate 将 velocity solve、position integration、collision、push solve 等按特定顺序组织;更改顺序会改变接触信息的新旧与稳定性。g_MaxPushCollisionIterations 是外层 push/collision 相关次数,不是通用 solver iteration。以这个常量推算所有约束成本会出错。
Sleep 同时降低模拟与接触活跃集合,但集中唤醒是另一类峰值。物理 substep 由游戏时间片策略决定,不是所有配置固定 60 Hz;性能实验要记录真实 dt、slice 数、迭代数和 active set,避免将质量变化误当优化收益。
建议测量:候选对→有效 manifold 比例、每 collider 接触数、phase 工作量分布、solver wait、约束残差、穿透、wake 数及异步 shape-test 量。查询预算应与仿真预算分开,减少 solver 不能消除大量镜头/AI/脚底射线成本。
9. 动画优化:控制语义与姿态质量必须分开#
动画成本包括状态更新、采样解码、混合、IK/表达式、骨架传播、物理交换、任务构图和等待。角色总数、总骨骼数和图编辑器总节点数都只是间接指标。
Motion tree composer 的有限 dependencies/frames/proxies 表明中间结果存活区间是第一类资源。过渡叠加、附属角色依赖和深层 blend 可同时放大计算与 scratch;应先削减不必要节点和重复映射,再考虑把每个节点拆得更细。
不可见角色仍可能需要时间、root motion、动画事件和物理骨骼。当前 AnimDirector 的特定入口没有简单遵照旧 mover-only 注释,不能将“按可见性跳过 pose”当作已普遍实现的事实。但新项目确实可以把状态 tick 与 pose 求值分级,前提是保证 late compose 不再推进时间,镜头突变时恢复 pose,并保留所有消费者需要的骨骼依赖闭包。
压缩率、采样缓存命中与 CPU 时间应联合考虑。人群同 clip 不同 phase 时可能不共享 block;随机 seek/切动作会打破顺序缓存。PoseMatcher 的线性扫描也不适合无测量地扩成巨大动作库。
推荐同时记录视觉质量:足底/手部误差、根轨迹、脚滑、事件重复/漏发和 ragdoll 过渡速度连续性。FPS 变高但玩法事件遗漏,不是有效优化。
10. 渲染优化:把提交与 GPU 成本拆开#
绘制列表、实例化、排序键和可见性筛选降低 CPU 提交成本;GBuffer、deferred lighting、阴影、透明和后处理消耗不同 GPU 资源。CPU drawlist pages 与 GPU draw call 数没有简单一一对应。
减少 draw calls 可能增加单批包围体、实例数据或过度绘制;更大 LOD 合并资源可能减少提交却放大 streaming 粒度;减少几何不一定降低大面积透明烟雾的像素成本。优化必须同时看 CPU build/submit、GPU pass 时间、可见像素和资源带宽。
HiZ/occlusion 使用历史或阶段结果时,要处理镜头跳变与对象运动带来的保守性。不能通过图形路径名字推断现代 GPU-driven/clustered 特性;本源码应按实际实现评价。
11. 跨系统过载会形成反馈环#
图描述可能的系统反馈,不是声称已测得现有游戏必然出现此环。其启发是:只给单个模块扩池或加线程,可能让系统更晚但更剧烈地触顶。
Audio voice 虚拟化、网络分批/限流、Fragment 禁止破坏标记都展示了局部降级思路。新项目需要统一优先级语义:玩家控制、关键碰撞、音频连续性、可选粒子和远景更新的损失容忍度不同。降级必须保持必要玩法结果,同时避免频繁升降质量形成抖动。
12. 观测体系:从计时条走向因果链#
源码已有 PF timer/value/timebar、trace、GPU marker、stall 检查和 Bank,但启用受配置约束;Timebar Core 默认回调为空,DeviceProfiler 的一个 IO 路径当前关闭。测量前应证明标记实际生效、时钟可比较、事件没有溢出。
建议每条异步工作携带关联 ID,记录:
- request/create:谁需要它,内容/对象 ID 与截止阶段。
- queued/ready:何时入队,依赖何时满足。
- start/end:在哪个线程/核心执行,实际服务时间。
- publish/consume:结果何时合法发布,谁等待了它。
- cancel/retire:取消意图与最后安全释放分别何时发生。
聚合统计应至少分 p50/p95/p99、最大值、超预算比例与连续超预算长度;平均 FPS 不能显示偶发半秒停顿。若线程区间重叠,不能把所有 CPU timer 相加当帧时;inclusive/exclusive 也不能混用。GPU 时间不能直接由 CPU 调用持续时间替代。
13. 针对新项目的实验矩阵#
以下仅为可执行的未来研究设计,本次没有运行。
| 实验 | 控制变量 | 重点观测 | 通过条件应如何定义 |
|---|---|---|---|
| Job 合批扫描 | 固定总计算,改变任务/批量 | 提交、queue age、最后任务、scratch | 帧尾收敛且吞吐不以更多等待换取 |
| 跨系统峰值 | 动画/物理/placement 同时突发 | runnable、锁、关键路径 | deadline 任务仍按时,可选任务有界退让 |
| 世界高速遍历 | 固定路线,改变速度/冷缓存 | ready p99、missing、IO/placement | 在目标速度下关键资源不迟到 |
| 瞬移与取消 | 连续请求不同区域并中断 | 在途字节、取消延迟、残留引用 | 内存最终回落,无陈旧发布 |
| 长期装卸 | 相同区域反复进出 | largest block、pin、defrag、reload | 峰值不持续爬升,无回收风暴 |
| 物理拓扑 | 相同刚体数,分散/堆叠/关节链 | pair、phase、误差、耗时 | 每类质量门槛均满足再比较速度 |
| 物理突发 | 破碎与全局唤醒 | pools、查询、solver wait | 容量失败可诊断,必要行为不丢失 |
| 动画多样性 | 同角色数,clip/phase/层数变化 | cache、活跃图、pose 峰值 | 质量与事件正确,资源上限明确 |
| 镜头突变 | 快速切镜与室内外切换 | scan、LOD、late pose、upload | 无长期缺块/错误姿态,峰值可界定 |
| GPU 覆盖率 | 固定粒子数改变屏幕面积 | pass 时间、overdraw、带宽 | 明确 CPU 与 GPU 限制切换点 |
| 音频与 IO | 世界冷装载时大量声音 | ring 水位、starved、解码成本 | 设备供给不因图形峰值中断 |
| 网络压力 | 控制丢包/抖动/码率 | queue age、state age、误差 | 最新状态收敛且内存有界 |
每轮比较应固定构建配置、内容版本、机器电源/频率策略、相机路径和采样窗口;记录 warm/cold 条件。先重复 baseline 确认波动范围,再比较更改,不以单帧最好值证明收益。
14. 优化排序与迁移决策#
建议先验证生命周期和容量,再按关键路径选择项目:
- 第一优先级:消除无意义工作、重复资源请求、同步强制装载、陈旧查询和错误引用。收益通常跨系统,且能改善稳定性。
- 第二优先级:工作集和数据布局,包含 LOD、sleep、图裁剪、量化、批量和中间结果复用。必须有质量门槛。
- 第三优先级:执行重叠、任务拓扑、发布阶段和服务优先级。需要全局时间线证明关键路径缩短。
- 最后精调:预取、intrinsic、亲和性和特定硬件路径。只对已定位热点做,并保留可比较的基线。
应迁移的是显式所有权、数据/实例分离、工具验证、阶段完成语义和高水位观测。应重新设计的是老硬件亲和性、SPU scratch 数值、光盘寻址启发式、共享可变 singleton 和依赖隐含于主循环的部分。
15. 源码证据与专题关联#
| 证据 | 本章使用的事实 |
|---|---|
| timebars.h | 28–35 细节配置与 no-op 回调;80–86 budget 传参 |
| profiler.h | __STATS、group/page、FlipElapsedTimes |
| stream_profiler.h | 14 IO profiler 关闭条件 |
| cache.h | 平台 cache line、对齐与 prefetch 分支 |
| streamingloader.cpp | 297 起主线程收尾;1603 起内存失败处理 |
| 多线程报告 | 调度结构、scratch、优先级与等待 |
| 内存报告、资源报告 | 分配器、known references、离线布局 |
| Streaming 报告、世界报告 | 依赖闭包、发布、defrag、LOD 和空间工作集 |
| 物理报告 | phase 冲突划分、时间片、sleep 与 shape tests |
| 动画报告 | 中间 frame、阶段完成、压缩与缓存 |
| 渲染报告、特效报告 | CPU/GPU 分离、SoA、透明与破坏峰值 |
| 音频报告、网络报告 | 连续 deadline、voice 预算、同步 age 和限流 |
这份源码最重要的启发是把性能问题写成可观察的因果链:谁产生工作,数据在哪驻留,等待哪项依赖,何时交付,以及过载时放弃什么质量。只复制某个局部技巧,无法复制整套系统的效果。