Skip to main content

Chapter 113: CPU-GPU Cost Analysis

实时渲染性能分析的第一步,是把一帧的耗时拆回 CPU 提交、GPU 执行、队列等待、呈现等待和工具证据。读完本章后,读者应能定位一帧慢在哪里,区分 CPU 负载和 GPU 负载,追踪 timer query 与 frame capture 之间的证据关系,并把 profiler 结果转成具体优化决策。

本章使用一帧“城市夜景场景”作为贯穿材料。这个 frame 包含 shadow map、G-buffer、deferred lighting、transparent particle、TAA resolve、bloom、UI 合成和 present。目标是 60 FPS,对应单帧预算约 16.67 ms;当前观察到的稳定帧耗时是 22 ms,画面症状是相机移动时持续掉帧,静止画面也没有恢复到目标帧率。

CPU-GPU cost analysis 讨论一帧从应用层构建命令到 GPU 完成图像输出的关键路径,并把单个函数耗时放回这条路径中解释。CPU 可能忙于场景遍历、动画更新、剔除、命令录制和资源上传;GPU 可能忙于 vertex、raster、fragment、compute、memory bandwidth、render target 写入和同步;present 还可能引入垂直同步节奏和交换链等待。性能结论只有放回这些路径中,才具备可迁移性。

本章的最终结论是:瓶颈识别应先确认 frame pacing 和 CPU/GPU 时间归属,再用 timestamp、marker、capture 和 counter 建立证据链,最后让优化动作对应具体 pass、资源格式、shader 路径或同步点。缺少证据链的优化会变成猜测;证据链完整时,性能预算可以被拆成可复查的工程决策。

113.1 渲染性能瓶颈是什么以及如何识别

渲染性能瓶颈是当前 frame 关键路径上最先耗尽预算的阶段。关键路径指一帧必须按顺序完成、且会影响最终显示时间的依赖链。它可能位于 CPU 侧,也可能位于 GPU 侧,还可能出现在 CPU 等 GPU、GPU 等资源状态转换、present 等显示节奏的交界处。

在城市夜景 frame 中,22 ms 的总耗时不能直接推出“GPU 慢”或“CPU 慢”。应用线程看到的 22 ms 可能包含等待交换链图像、等待上一帧 fence、等待资源上传完成、等待垂直同步,或真正执行本帧 update 和 command recording 的时间。GPU 侧看到的 22 ms 也可能由一个 heavy lighting pass 主导,或由多次 render target 切换、MSAA resolve、texture bandwidth 和透明粒子 overdraw 叠加出来。

瓶颈识别的第一条规则,是先把目标帧率转换成 frame budget。60 FPS 的预算是 16.67 ms,120 FPS 的预算是 8.33 ms,30 FPS 的预算是 33.33 ms。预算决定了“慢”的判断标准:一个 10 ms 的 lighting pass 在 60 FPS 下还能留出空间,在 120 FPS 下已经占用整帧预算的大部分。

第二条规则,是先判断慢帧是否稳定。稳定慢帧通常指每帧都在 20 ms 左右波动,说明关键路径持续超预算;尖峰慢帧通常指偶发上升到 40 ms 或 80 ms,常见原因是 shader 编译、资源 streaming、pipeline state 创建、同步 readback、操作系统调度或垃圾回收。二者对应的证据不同,稳定慢帧适合按 pass 分解,尖峰慢帧适合先找一次性事件。

第三条规则,是区分“工作耗时”和“等待耗时”。工作耗时代表 CPU 或 GPU 正在执行可归因任务,例如 draw submission、shader execution、texture sampling、render target write。等待耗时代表某一侧没有执行有效工作,例如 CPU 等 fence、GPU 等 barrier 之前的资源状态、present 等刷新节奏。优化工作耗时应改数据路径或 shader;优化等待耗时应改同步、队列组织或帧间资源生命周期。

可以用一个最小症状表建立初步判断:

观察现象高概率位置需要补充的证据
CPU frame time 高,GPU timestamp 低CPU 提交或等待路径CPU profiler、queue wait、fence wait、present wait
GPU timestamp 高,CPU 录制时间低GPU pass 或 shader 路径pass timing、draw event、counter、resource bandwidth
CPU 与 GPU 都高双侧都有工作,或 CPU 在等待 GPUtimeline、frame queue 深度、fence 和 present 状态
平均帧正常但有尖峰一次性资源、编译、同步或 streamingspike frame capture、日志 marker、资源创建时间
present wait 高但 GPU 时间低显示节奏或 swapchain pacingvsync 设置、交换链缓冲数量、队列提交时间

这个表只能生成假设,不能直接生成优化结论。瓶颈识别必须继续进入测量:CPU timer 回答主线程和渲染线程花了多少时间;GPU timer 回答命令在 GPU 时间域中执行了多久;frame capture 回答哪个 pass、哪个 draw、哪个资源状态或 shader 片段贡献了成本;counter 回答瓶颈更偏 ALU、texture、bandwidth、occupancy、overdraw 还是同步。

城市夜景 frame 的初步观察是:CPU update 和 command recording 合计 5.1 ms,GPU timestamps 覆盖的主渲染区间是 20.4 ms,present wait 接近 0.3 ms。这个证据说明当前慢帧主要由 GPU 执行路径决定,CPU 侧尚未成为主导瓶颈。后续分析应把重点放到 GPU pass timing、shader cost、render target 格式、overdraw 和资源转换上。

113.2 CPU 与 GPU 负载的区分与测量方式

CPU 与 GPU 的测量必须承认二者异步执行。CPU 录制命令后通常把 command buffer 或 command list 提交到队列,GPU 在之后的时间点执行这些命令。CPU 一帧的耗时和 GPU 一帧的耗时可以重叠;应用层看到的 frame time 是最终显示节奏上的结果,不等同于 CPU 工作时间,也不等同于 GPU 工作时间。

最小测量链可以拆成五个时间段:CPU update、CPU render preparation、queue submit、GPU execution、present pacing。CPU update 包括输入、动画、物理、脚本和场景状态更新。CPU render preparation 包括可见性计算、排序、batch 构建、descriptor 更新、command recording。queue submit 把 CPU 准备好的命令送入 GPU 队列。GPU execution 执行 render pass、compute pass、copy、resolve 和 barrier。present pacing 处理交换链、显示刷新和帧节奏。

下面的图用于约束分析顺序。它表达的是测量对象之间的依赖关系,不表达某个 API 的固定实现。

CPU timer 适合回答“应用在 CPU 上主动执行了多少工作”。它应放在可命名的阶段外侧,例如 UpdateSceneBuildRenderItemsRecordShadowPassSubmitFrame。如果 CPU timer 把等待 fence、等待交换链图像和正常命令录制混在一个大区间中,结果会失去诊断价值。CPU timer 需要把等待区间单独标记,等待区间的修复方式通常来自资源生命周期和队列组织。

GPU timestamp 适合回答“GPU 在某段命令之间消耗了多少设备时间”。Vulkan 的 vkCmdWriteTimestamp 文档把它定义为向 query object 写入 device timestamp,并且 timestamp 与 command buffer、pipeline stage、query pool 和 queue family 能力相关。Direct3D 12 的 Timing 文档说明 timestamp query 结果需要结合 command queue frequency 转换成秒,并且 direct queue 与 compute queue 支持 timestamp。这个边界说明 GPU 时间来自设备时间域,CPU 时间来自主机时间域,二者需要通过工具或 API 提供的校准机制关联。

command queue wait 是 CPU-GPU 交界处的关键证据。CPU 可能在提交前等待上一帧 fence,因为同一块 uniform buffer、staging buffer、descriptor heap 或 command allocator 还在被 GPU 使用。GPU 也可能在队列中等待前一个提交释放资源状态,或等待 compute queue 与 graphics queue 之间的 semaphore / fence。此类等待的症状是时间线上存在空隙或长时间阻塞,但对应区间没有大量 shader 执行。

present wait 需要从显示节奏中拆出来。开启垂直同步时,present 可能等待下一次刷新;交换链缓冲数量不足时,CPU 可能等待可写 back buffer;GPU 完成很快时,present wait 高不代表渲染 pass 慢;GPU 超预算时,present wait 反而可能很低,因为 GPU 已经错过刷新窗口。判断 present 的核心证据是 frame pacing、swapchain image acquire、queue present 和显示刷新之间的关系。

城市夜景 frame 的测量可以按如下顺序执行。先用 CPU timer 得到 UpdateScene = 1.4 msBuildRenderItems = 1.1 msRecordCommands = 2.0 msSubmitFrame = 0.6 ms。再用 GPU timestamp 包围主要 pass,得到 Shadow = 1.3 msGBuffer = 4.6 msLighting = 8.9 msTransparent = 2.2 msTAA = 2.1 msBloom = 1.0 msUI = 0.3 ms。最后看 profiler timeline,确认 GPU 队列几乎全程繁忙,CPU submit 后没有长时间 fence wait。这个证据链把瓶颈定位到 GPU execution,下一步应进入具体 pass。

113.3 Timer Query GPU Marker and Frame Cost Measurement

Timer query 与 GPU marker 的组合负责把一帧拆成可命名、可比较、可复查的成本段。timer query 给出时间数值,GPU marker 给出事件语义。只有时间没有名字,无法知道耗时属于哪条渲染路径;只有名字没有时间,无法判断预算压力。

最小 marker 结构应匹配 frame graph 或 pass graph。城市夜景 frame 可以按 FrameShadowMapGBufferDeferredLightingTransparentParticlesTAAResolveBloomUICompositePresent 命名。marker 名称需要稳定,因为后续对比不同版本、不同质量等级和不同相机位置时,工具会依赖同一组事件名进行归因。

GPU timer 的放置位置应贴近要回答的问题。包围整个 frame 可以回答 GPU 主渲染区间是否超预算;包围 pass 可以回答哪个 pass 贡献最大;包围 draw group 可以回答某类材质、某类透明物或某组阴影 caster 是否异常;包围 compute dispatch 可以回答 culling、TAA、denoising、particle update 或 upscaling 的实际成本。timer 的粒度越细,测量开销和数据噪声越需要控制,因此常规做法是先按 pass 粒度定位,再对异常 pass 内部加细。

下面的伪代码展示了 marker 与 timestamp 的组织方式。它表达测量结构,不绑定某个具体图形 API。

BeginFrameMarker("Frame");
WriteGpuTimestamp("FrameBegin");

BeginGpuMarker("ShadowMap");
WriteGpuTimestamp("ShadowBegin");
RecordShadowPassCommands();
WriteGpuTimestamp("ShadowEnd");
EndGpuMarker();

BeginGpuMarker("GBuffer");
WriteGpuTimestamp("GBufferBegin");
RecordGBufferPassCommands();
WriteGpuTimestamp("GBufferEnd");
EndGpuMarker();

BeginGpuMarker("DeferredLighting");
WriteGpuTimestamp("LightingBegin");
RecordDeferredLightingCommands();
WriteGpuTimestamp("LightingEnd");
EndGpuMarker();

WriteGpuTimestamp("FrameEnd");
EndFrameMarker();

timestamp 的结果需要转换成毫秒。Vulkan 常见路径使用设备属性中的 timestamp period,把 tick 差值转换成纳秒,再转换成毫秒。Direct3D 12 常见路径使用 command queue frequency,把 tick 差值除以每秒 tick 数。二者的共同原则是:同一段 GPU 时间必须来自同一时间域,同一队列上的 timestamp 才能直接相减;跨队列、跨 CPU-GPU 时间域和跨设备的比较需要使用 API 或工具提供的校准机制。

GPU marker 还承担工具导航作用。RenderDoc、Nsight、PIX、Xcode GPU tools 或厂商 profiler 在 event list / timeline 中显示 marker 后,开发者可以从 DeferredLighting 直接跳到对应 draw、pipeline state、resource binding 和 shader。marker 的价值在于把“8.9 ms lighting”连接到具体 render target、G-buffer texture、light list buffer、BRDF shader 和全屏 draw。

CPU timer 仍然需要存在。GPU timestamp 能说明 GPU 上的命令执行时间,不能说明 CPU 在构建 light list、更新 descriptor、拷贝 constant buffer 或等待 fence 上消耗了多少时间。城市夜景 frame 中,如果 BuildRenderItems 从 1.1 ms 上升到 7 ms,而 GPU lighting 仍然是 8.9 ms,那么优化方向会从 shader 转向场景数据组织、可见性列表、排序、分配器和命令录制。

测量还需要控制采样条件。相机位置、分辨率、渲染质量、阴影距离、粒子数量、窗口是否被遮挡、刷新率、垂直同步、动态分辨率和后台进程都会影响结果。可复查测量应固定场景输入,至少记录分辨率、质量档位、帧号区间、GPU/CPU 型号、驱动版本、API backend 和是否开启调试层。章节正文不能声称某个数字代表读者本地机器;数字只用于展示判断路径。

在城市夜景 frame 中,timer query 给出的 pass 数据说明 lighting 是最大成本段,但它还没有说明 lighting 为什么贵。下一节需要进入 frame capture,把 pass timing、draw event、resource state、shader cost 和 counter 连接起来。

113.4 Frame Capture Pass Timing and Bottleneck Evidence Walkthrough

Frame capture 的目标是把时间数字落到一帧内部的事件、资源和状态。timer query 已经告诉我们 DeferredLighting = 8.9 ms,capture 需要回答这 8.9 ms 是由 shader ALU、texture fetch、G-buffer bandwidth、light loop、overdraw、render target 格式、barrier 或无效 draw 造成的。

一次有效的 capture walkthrough 应从 frame 级别进入 pass 级别,再进入 draw 或 dispatch 级别。先看 event browser 或 timeline,确认 frame 中有哪些 pass,哪些 pass 占用时间最大。再打开异常 pass,观察 draw 数量、dispatch 数量、pipeline state、render target、depth/stencil、blend、descriptor / binding、texture 格式和 buffer。最后结合 shader 统计、pipeline statistics、hardware counter 或工具提供的热点报告,判断成本来源。

城市夜景 frame 的 capture 可以这样读:

PassGPU 时间关键资源初步判断
ShadowMap1.3 msdepth atlas成本正常,caster 数量可控
GBuffer4.6 msalbedo、normal、material、depthrender target 写入和几何覆盖偏高
DeferredLighting8.9 msG-buffer、light list、HDR target主要瓶颈候选
TransparentParticles2.2 msparticle texture、HDR target透明 overdraw 需要复查
TAAResolve2.1 mshistory、motion vector、depth全屏采样成本中等
Bloom1.0 msdownsample chain成本可接受
UIComposite0.3 msUI atlas成本可接受

这个表的作用是缩小排查范围。DeferredLighting 占用整帧 GPU 时间的约 44%,并且它是全屏 pass,输入包括多张 G-buffer、light list 和阴影结果。全屏 pass 的成本通常与分辨率、纹理读取次数、输出格式、分支、light loop 和 cache behavior 相关。下一步应打开该 pass 的 draw event。

在 draw event 层,先确认 pipeline state。DeferredLighting 使用一个 full-screen triangle,depth test 关闭,blend 关闭,输出到 RGBA16F HDR target。输入纹理包括 normal、albedo、material、depth、shadow mask 和 SSAO。shader 里每个像素读取 G-buffer,重建 view-space position,遍历 tile light list,计算 BRDF,并写入 HDR color。这个状态说明 vertex 成本可以忽略,fragment shader 和 memory bandwidth 是主候选。

资源状态也会影响证据判断。G-buffer 在上一 pass 作为 render target 写入,lighting pass 作为 sampled texture 读取,需要一次 render target 到 shader resource 的状态转换。HDR target 在 lighting pass 写入,后续 TAA 和 bloom 继续读取。若 frame capture 显示 barrier 过多、资源在多个 queue 之间反复转移、或每个小 pass 都触发 layout transition,成本可能来自资源生命周期。若 barrier 时间很短,主成本仍应回到 shader 和 bandwidth。

shader cost 的判断需要看执行粒度。full-screen lighting 的 fragment invocation 数量接近屏幕像素数;在 2560×1440 分辨率下,单个全屏 pass 约 368 万像素。每个像素如果读取 6 到 10 次纹理,再执行多光源循环,成本会快速累积。若工具 counter 显示 texture busy 或 memory bandwidth 高,优先检查 G-buffer 格式、采样次数、压缩、normal encoding 和 light list 访问模式。若 ALU busy 高,优先检查 BRDF 分支、阴影过滤、数学函数、动态循环和光源数量。

透明粒子 pass 的 2.2 ms 也需要判断边界。透明对象通常开启 blend,depth write 关闭,overdraw 会让同一像素被多次 shading 和 blending。若 capture 的 overdraw view 显示屏幕中心有大量粒子叠加,并且 transparent pass 的 color target bandwidth 高,那么优化方向应是粒子裁剪、软粒子半径控制、分辨率降低或合并粒子贴图采样。若透明成本只在特定爆炸效果出现,则它属于尖峰成本,质量降级策略可绑定到粒子密度或屏幕面积。

frame capture 的关键输出是一条证据链:22 ms frame → GPU execution 20.4 ms → DeferredLighting 8.9 ms → full-screen fragment path → 多 G-buffer texture fetch + light loop → bandwidth / shader counter 支持。这条链让优化动作可以被复测。修改 G-buffer 格式后,应观察 lighting pass 的 texture bandwidth 和 GPU time;修改 light culling 后,应观察 light list 长度、循环次数和 lighting pass 时间;降低分辨率后,应观察全屏 pass 是否按像素数量近似缩放。

113.5 Profiling Evidence to Optimization Decision

优化决策必须从证据映射到资源路径。pass timing 说明哪里耗时,counter 说明哪类硬件资源受压,capture 说明哪些 API 状态、shader 输入和资源格式参与了该成本。只有这三类证据对齐,优化才有稳定方向。

城市夜景 frame 的主要证据指向 deferred lighting。可选优化包括 pass 合并、资源格式调整、shader 简化、同步减少和质量降级。它们作用在不同路径上,适用条件也不同。

证据优化决策改变的路径复查指标
全屏 pass 多次读取 G-buffer,bandwidth 高压缩或重排 G-buffer 格式减少 texture read 和 render target 存储带宽bandwidth counter、lighting pass time、画质误差
light loop 平均长度高,ALU busy 高改进 tiled / clustered light culling减少每像素光源循环次数light list 长度、shader instruction、GPU time
多个小后处理 pass 频繁读写 HDR target合并 pass 或使用更紧凑中间格式减少 full-screen read/write 和 barrierpass count、render target traffic、barrier 时间
CPU 等待上一帧资源 fence增加 per-frame resource ring降低 CPU-GPU 同步等待fence wait、CPU frame time、queue idle
透明粒子局部 overdraw 高粒子屏幕面积控制或低分辨率透明路径减少 blend 和 fragment invocationoverdraw view、transparent pass time、视觉差异
TAA / bloom 在高分辨率下线性增长动态分辨率或质量档位控制像素级后处理成本resolution scale、GPU frame time、稳定帧率

pass 合并适合处理多个相邻全屏 pass 频繁读写同一组中间纹理的情况。例如 bloom 的 downsample、threshold 和 blur 如果拆成过多 pass,会增加 render target 读写和状态切换。合并的收益来自减少内存流量和 pass 边界开销;风险是 shader 变复杂、寄存器压力上升、调试粒度下降。复查时应同时看 GPU time、bandwidth counter 和 shader occupancy。

资源格式调整适合处理 bandwidth 主导的 pass。G-buffer 中的 normal 可以使用压缩编码,material 参数可以打包到更少通道,HDR target 可以在画质允许时从更高精度格式降到较低精度格式。这个决策必须绑定视觉误差复查:normal banding、roughness 精度、specular 闪烁、HDR bloom 阈值和色带都可能暴露格式调整的副作用。资源格式优化的正确证据是带宽下降、pass 时间下降、画质误差在目标范围内。

shader 简化适合处理 ALU 或 texture fetch 主导的 draw。deferred lighting 中可以减少昂贵数学函数,重排分支让同类材质走同一路径,预计算可复用项,降低阴影过滤采样数,或把复杂材质拆到单独 quality tier。shader 简化需要保持输入输出语义稳定:输入仍然来自 G-buffer、light list 和 shadow mask,输出仍然写入 HDR target。若输出语义改变,后续 TAA、bloom 和 tone mapping 都需要同步复查。

同步减少适合处理 queue idle、fence wait、readback 和资源状态反复转换。典型动作包括延迟 GPU readback、使用多帧环形 buffer、把一次性上传集中到 upload pass、减少 graphics/compute queue 之间的交叉等待、让 frame graph 统一管理 resource transition。同步优化的证据应落到 timeline 中 idle gap 变短、CPU fence wait 降低、GPU queue occupancy 更连续。

质量降级适合在目标平台或目标分辨率下仍然超预算时使用。动态分辨率、阴影距离缩短、透明粒子密度降低、TAA sample 策略调整、bloom half resolution 和 light count cap 都属于质量预算决策。它们应由 frame budget 驱动:当 GPU frame time 连续多帧超过 16.67 ms,系统降低某个像素级或 overdraw 级成本;当时间回到预算内,再逐步恢复质量。质量降级的目标是稳定 frame pacing,而非追求单帧最低耗时。

最终决策应形成可复测闭环。以城市夜景 frame 为例,第一轮选择把 G-buffer material 从四通道 16-bit 浮点压到两通道 8-bit normalized,并把 lighting shader 中的阴影过滤从 9 taps 降到质量档位控制的 4 taps。复测后,如果 DeferredLighting 从 8.9 ms 降到 6.1 ms,GPU frame 从 20.4 ms 降到 17.5 ms,仍略超 60 FPS 预算,那么第二轮再处理 transparent particles 或动态分辨率。每轮只改少量变量,才能把结果归因到对应证据。

最小自检任务

给定一个 60 FPS 目标的渲染 frame,记录如下:CPU update 1.8 ms,CPU command recording 2.7 ms,CPU submit 0.4 ms,CPU fence wait 0.2 ms;GPU timestamp 显示 shadow 1.0 ms,G-buffer 3.8 ms,lighting 9.6 ms,transparent 3.1 ms,TAA 1.9 ms,bloom 0.8 ms,UI 0.2 ms;present wait 0.1 ms。frame capture 中 lighting 是 full-screen pass,读取 normal、albedo、material、depth、shadow mask,输出 RGBA16F HDR target;transparent pass 的 overdraw view 显示屏幕中心粒子重叠严重。请判断当前主瓶颈位置、第一轮优化对象、需要复查的证据,以及一个不应优先投入的方向。

答案要点

主瓶颈位于 GPU execution。CPU 主动工作约 4.9 ms,fence wait 和 present wait 都很低,GPU pass 合计约 20.4 ms,已经超过 16.67 ms 预算。第一轮优化对象应优先选择 lighting pass,因为它占 9.6 ms,是最大成本段,并且 full-screen 读取多张 G-buffer 和写入 RGBA16F HDR target,符合 bandwidth 与 fragment shader 路径主导的特征。复查证据应包括 lighting pass 的 texture bandwidth、shader ALU / texture counter、G-buffer 格式、light loop 长度、输出格式和修改前后的 pass timing。transparent pass 是第二候选,因为 overdraw view 已经显示局部粒子重叠,复查时应看 fragment invocation、blend bandwidth 和屏幕覆盖面积。CPU 多线程 command recording 不应作为第一优先级,因为当前 CPU 时间远低于 GPU 时间,且等待区间很小;先优化 CPU 录制无法直接释放 20.4 ms 的 GPU 关键路径。

本章知识点总结

  • 瓶颈定义:渲染瓶颈是当前 frame 关键路径上最先耗尽预算的阶段。
  • 预算基准:60 FPS 对应约 16.67 ms,所有成本判断都应先放回目标帧率。
  • 稳定慢帧:稳定超预算适合按 pass 分解,尖峰超预算适合先查一次性事件。
  • 工作等待:工作耗时对应真实执行,等待耗时对应 fence、queue、barrier 或 present 节奏。
  • 异步执行:CPU 时间和 GPU 时间可以重叠,应用 frame time 需要拆成多个时间域理解。
  • CPU 测量:CPU timer 应把 update、准备、录制、提交和等待区间分开命名。
  • GPU 测量:GPU timestamp 应按 frame、pass、draw group 或 dispatch 粒度逐层加细。
  • Marker 价值:GPU marker 把时间数字连接到具体 pass、draw、pipeline state 和资源绑定。
  • Capture 证据:frame capture 应把 pass timing、draw event、resource state、shader cost 和 counter 连成证据链。
  • Lighting 判断:full-screen lighting 成本通常与像素数、G-buffer 读取、light loop、输出格式和 shader 分支相关。
  • 同步判断:queue idle、fence wait 和 barrier 时间需要用 timeline 观察,优化动作应指向资源生命周期。
  • 决策闭环:优化应从证据映射到资源路径,并通过同一组 timer、marker、capture 和 counter 复测。