GPU Gems 1 · Chapter 28. Graphics Pipeline Performance
用可证伪的 workload 探测定位 graphics pipeline 瓶颈,再针对 ROP、纹理、fragment、vertex 或 CPU 阶段优化并复测。
学习目标
- 能画出 CPU、vertex/index fetch、vertex、fragment 和 ROP 阶段,并说明每阶段的主要工作量
- 能用单变量 workload 或时钟探测区分 frame-buffer、texture、fragment、vertex 和 CPU 瓶颈
- 能根据已证实的瓶颈选择减少锁、扩大 batch、优化顶点缓存、early-z 或纹理压缩等杠杆
- 能在同一 workload、目标硬件和画质约束下复测,确认优化收益不是测量噪声或阶段转移
图形性能优化最容易犯的错误,是把“某段代码看起来复杂”当成瓶颈。GPU 是多个专用单元组成的异构流水线;一个阶段的局部加速只有在它确实限制整帧吞吐时才有收益。GPU Gems 1 第 28 章把工作组织成一个可重复的循环:定位、优化、再次定位。
↡由 CPU 提交和 GPU 多个功能单元串联组成、吞吐受最慢阶段限制的渲染处理链 的顶层划分是 CPU 与 GPU;GPU 内部还包括顶点和索引获取、vertex processing、fragment shading 与 raster operations。理解阶段边界,才能把“帧率低”翻译成可测试的假设。
1. 用瓶颈循环替代猜测
↡限制当前整条渲染流水线吞吐的最慢阶段或资源约束 定位的第一步是改变一个阶段的 workload,或改变与该阶段相关的计算能力,例如 core 或 memory clock;如果性能随之变化,就得到该阶段是瓶颈的证据。第二步才是减少该阶段工作,直到性能不再改善或达到目标;第三步重新测量,因为优化后瓶颈可能转移。
这个方法还要注意帧内的空间差异:一个低多边形 skybox 可能受 fragment 或 frame-buffer 访问限制,而只覆盖少量像素的 skinned mesh 可能受 CPU 或 vertex processing 限制。单个 primitive 有一个主要瓶颈,但整帧的瓶颈可能随对象、材质和视图变化,因此应支持 object-by-object 或 material-by-material 的对照。
2. 从 ROP 和 frame-buffer bandwidth 开始
↡负责深度和模板读写、比较、颜色读写、alpha blending 与 testing 的渲染管线末端阶段 处理深度、模板、颜色和混合,工作量主要消耗 frame-buffer bandwidth。一个直接的探测是只改变 color 或 depth buffer 的位深:如果从 32-bit 降到 16-bit 能显著改善性能,就有理由怀疑帧缓冲带宽;关联的 GPU memory clock 变化也应产生一致方向的响应。
baseline: color = 32-bit, depth = 32-bit
probe: color = 16-bit, depth = 16-bit
hold: camera, primitives, shaders, resolution, workload
observe: frame time, memory clock response, depth precision artifacts不要把精度降低直接当成发布修复。先证明 ROP 是瓶颈,再评估 16-bit depth/color 是否满足场景的 z 范围、透明度、HDR 和后处理需求。
3. 区分 texture bandwidth 和 fragment shading
↡纹理 fetch 从内存取数据所消耗的带宽,受纹理尺寸、格式、mipmap 和缓存局部性影响 发生在 texture fetch 触发内存请求时。与其随意改纹理格式,不如对测试场景施加较大的正 mipmap LOD bias,使用粗 mip 级别降低等效纹理尺寸;若性能明显改善,纹理带宽就是重要嫌疑。
↡执行 fragment 或 pixel shader、为每个生成的 fragment 计算颜色和深度的阶段 与 frame-buffer bandwidth 都常被统称为 fill rate,但两者不是同一阶段。已经用位深探测排除 ROP 后,再改变分辨率或 fragment program 的有效长度;如果性能随分辨率或真实 shader 工作变化,才能把证据指向 fragment shading。实验指令必须有真实输出依赖,避免被驱动消除。
float4 shade(float2 uv, Texture2D source, SamplerState samplerState) {
float4 value = source.Sample(samplerState, uv);
float3 edge = source.Sample(samplerState, uv + float2(0.001, 0.0)).rgb - value.rgb;
return float4(value.rgb + edge * edgeGain, value.a);
}定位后,fragment 优化可以考虑 depth-first 和 early-z、把复杂函数放进 lookup texture、把可在线性插值的 per-fragment 工作移到 vertex、降低不必要精度、减少过度 normalization、降低远处 shader LOD,以及不需要时关闭 trilinear filtering。每一项都要以同一场景和画质对照验证。
4. 检查 vertex processing 与 vertex/index transfer
vertex processing 的成本取决于每个 vertex 的工作和处理的 vertex 数量。改变 vertex program 的有效长度或固定功能中的 specular lighting、texture-coordinate generation 等工作,可以测试它是否是瓶颈;无效 no-op 同样不可靠。
顶点和索引获取是 GPU 部分的第一步,数据可能在 system memory,也可能在 local frame-buffer memory。改变 vertex format size 可以探测 transfer 压力;如果数据在 system memory,AGP/PCI Express 速率更相关;如果在本地显存,memory clock 更相关。若这些探测都不影响性能,CPU bound 就成为更合理的假设。
struct VertexCompact {
float position[3];
unsigned short normalOcta[2];
unsigned short uv[2];
};
// 以 indexed draw 复用顶点,并在同一 workload 下比较紧凑格式的帧时间。
drawIndexed(mesh.indexBuffer, mesh.vertexBuffer, mesh.indexCount);vertex 侧的优化包括使用 indexed primitives 提高 post-T&L vertex cache 命中、减少 vertex 数量和 LOD、使用紧凑格式与 16-bit indices、让访问尽量顺序、把每对象或每帧计算移到 CPU,以及选择更便宜的坐标空间。不要为了省 transfer 带宽把本应由 shader 计算的高频属性硬塞成大量顶点数据。
5. 优化 CPU 锁、batch 和 GPU 等待
同步访问 GPU 正在使用的资源,会让 CPU 等待深流水线清空,GPU 也因停顿和重新填充损失周期。典型风险包括读回先前渲染的 surface,或写入 GPU 正在读取的 texture、vertex buffer。发布代码应尽量避免 CPU 与 GPU 同时争用同一资源,并把 readback 安排到明确的同步边界。
batch 是一次 API draw call 提交的 primitive 集合。对相同材质和状态,扩大 batch 或减少 batch 数量可以降低 CPU API 调用成本。↡一次 draw call 提交的一组共享足够材质和状态的 primitives,批次越大通常越能摊薄 CPU 提交开销 的增长可以来自 texture pages、合适的 triangle strip stitching、shader branching、常量内存中的矩阵查表,以及把材质决定尽量推迟到更靠后的阶段。
这些动作不是无条件免费:texture pages 要评估 mipmapping 和 antialiasing,shader branching 会增加 shader cycles,常量查表需要额外索引和一致布局。优化目标是减少总 frame time,不是让某个计数器单独变漂亮。
6. 带宽和画质的共同边界
纹理带宽受尺寸、格式、缓存和 mipmap 共同影响。只要目标表面会 minify,就应使用 mipmapping 来改善 alias 与 cache locality;如果纹理只是 decal 或 detail,可考虑 DXT1、DXT3 或 DXT5 等压缩。64-bit 或 128-bit floating-point texture 只有在必要时才值得付出带宽成本。
frame-buffer 优化可从 depth-first、减少 alpha blending、关闭不必要的 depth writes、避免多余 color clear、粗略 front-to-back 和针对 skybox 的策略开始。floating-point frame buffer、multiple render target 和 32-bit depth 都有额外带宽成本,但不能在 HDR、精度或透明度要求未验证时盲目降成 16-bit。
Graphics Pipeline Performance Lab
先预测:哪个变量能证明当前阶段是瓶颈?
先选阶段,再切换探测负载;若性能响应显著,才有理由把优化预算投入该阶段。
实验只表达“改变一个探测变量后,性能响应是否跟随”的因果方向,不是假装采集真实 GPU 计时。发布报告应保存硬件、驱动、分辨率、场景、shader 版本、workload、基线帧时间、探测帧时间、画质差异和重复次数。
小结:定位—优化—复测
- graphics pipeline 由多个并行专用阶段组成,整帧吞吐受当前 bottleneck 限制。
- ROP 用 buffer 位深和 memory clock 探测;texture bandwidth 用正 LOD bias 探测;fragment 用分辨率或真实 shader 工作探测。
- vertex processing 与 transfer 可通过 vertex program、format size、indexed cache 和数据位置对照;探测无响应时再考虑 CPU bound。
- CPU 侧应避免锁住 GPU 正在使用的资源,并通过 batch size、texture pages 和延迟决策摊薄提交成本。
- 每次优化后都要在同一 workload 和画质约束下复测,并检查瓶颈是否转移,而不是只看局部指标。
练习
问题 1|实验设计 要判断一个场景是否受 frame-buffer bandwidth 限制,哪一个单变量探测最合适?哪些变量必须保持不变?
问题 2|归因题 提高正 mipmap LOD bias 后性能上升,而改变 fragment shader 长度没有变化。哪个阶段更可疑?为什么不能直接说“fragment 不贵”?
问题 3|优化题 发现 CPU 提交是瓶颈,同时场景有许多共享材质但矩阵不同的小物体。给出至少三项优化,并列出需要复测的副作用。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- graphics pipeline
- 由 CPU 提交和 GPU 多个功能阶段组成、吞吐受最慢阶段限制的渲染处理链。
- bottleneck
- 限制当前整条渲染流水线吞吐的最慢阶段或资源约束。
- raster operations
- 负责深度、模板、颜色读写及 alpha 测试和混合的管线末端阶段。
- texture bandwidth
- 纹理 fetch 从内存取数据消耗的带宽,受尺寸、格式、mipmap 和缓存影响。
- fragment shading
- 执行 fragment shader、为每个 fragment 计算颜色和深度的阶段。
- batch size
- 一次 draw call 提交的一组共享足够状态的 primitives 数量。