Chapter 3: Memory Hierarchy and Caches
读完本章后,读者应能把一帧图形性能问题追踪到具体内存路径:数据从 register、shared/local memory、texture cache、L1/L2、VRAM 或 unified memory 中哪一层进入 shader,访问是否连续,缓存是否能复用,带宽是否被 render target 写入、纹理采样、buffer 更新或同步等待消耗掉。
本章使用一个贯穿材料:一帧材质预览画面。画面中有一组网格模型,每个模型读取 albedo、normal、roughness、metallic 四类纹理,几何 pass 写入 G-buffer,lighting pass 读取 G-buffer 和材质贴图,最后进行 tone mapping 和 resolve。这个 frame 的画面症状可能是转动相机时帧时间上升、贴图变清晰后 GPU 时间突然增加、打开 MSAA 后 resolve 成本上升,或者 compute pass 写 buffer 后 fragment pass 等待。
内存层级的主问题是:同样一条 shader 指令,为什么有时只是从 register 读取一个值,有时会触发一次跨 cache、跨显存或跨统一内存的等待。GPU 的算术吞吐通常很高,许多真实 frame 的瓶颈来自数据到达执行单元的速度。判断图形性能时,需要把“shader 慢”拆成三个可观察问题:读了多少字节,访问顺序是否让缓存和内存事务有效工作,写入或同步是否让后续 pass 等待。
本章的结论先给出:内存优化的起点是数据路径图,随后才是具体技巧。先定位资源类型和管线阶段,再判断访问模式,再用工具观察 bandwidth、cache miss、memory transaction、stall 或 pass time,最后选择布局、压缩、mipmap、tile/local memory、批量上传或 pass 合并等动作。没有这个顺序,优化容易停留在经验口号上。
3.1 GPU Memory Hierarchy Access Cost Map
GPU 内存层级可以看成一张“距离执行单元多远”的成本图。shader 核心正在执行一条指令时,最便宜的数据位于当前线程可直接使用的位置,最昂贵的数据需要穿过多级 cache、显存控制器,甚至经过 CPU/GPU 共享内存的一致性路径。这里的成本不应只理解成单次延迟,同时要看吞吐、并发隐藏能力和是否阻塞同一组线程束。
贯穿 frame 中的 fragment shader 会读取法线贴图、粗糙度贴图和 G-buffer。若法线向量已经被解包到临时变量,后续光照计算读取的是 register。若同一像素需要从 normal texture 采样,数据路径通常会经过 texture unit、texture cache、L1/L2,再到显存或 unified memory。若 lighting pass 读取上一 pass 写出的 G-buffer,还要加上 render target 写入、layout/state transition、cache flush 或 tile resolve 的成本边界。
| 层级 | 图形语境中的典型对象 | 成本判断 | 常见风险 |
|---|---|---|---|
| Register | shader 临时变量、向量分量、循环中复用的中间值 | 最贴近执行单元,访问延迟最低 | register 数量过高会降低 occupancy,溢出后可能转成更慢的内存访问 |
| Shared / local memory | CUDA shared memory、Metal threadgroup memory、OpenCL local memory、tile 内共享数据 | 适合同一 thread group 内复用和交换数据 | 需要显式组织生命周期,同步过多会形成等待,bank conflict 会拉低吞吐 |
| Texture cache | 纹理采样、sampler 过滤、mipmap 读取 | 适合二维空间局部性和相邻像素相似采样 | 随机 UV、过高各向异性、无 mipmap 的远处纹理会拉高采样成本 |
| L1 / L2 cache | buffer load、texture backing store、render target 数据、通用 cache 路径 | 命中时吸收重复访问,未命中时请求下一级 | 访问跨度大、stride 过宽、跨 pass 无复用会降低命中率 |
| VRAM | 独显上的 texture、buffer、render target、staging copy 目标 | 容量和带宽高,延迟高于片上 cache | 大量纹理读取、MSAA render target、copy/resolve 会形成带宽压力 |
| Unified memory | 集成 GPU 或统一内存平台上的共享物理内存 | CPU/GPU 共享数据更直接,复制路径更短 | CPU/GPU 争用、缓存一致性、资源同步仍会产生等待 |
这张表只能作为阅读顺序,不能当作固定数值表。NVIDIA、AMD、Apple 等架构在 cache 大小、tile memory、统一内存、纹理单元和压缩路径上差异明显;同一 API 的不同后端也会改变实际成本。工程判断应固定在可观察对象上:当前 pass 读的是 texture 还是 buffer,访问是否连续,是否跨越 render target 读写边界,工具是否显示 memory throughput 或 stall 上升。
register 的风险经常被低估。一个 fragment shader 中临时向量过多、分支路径保存状态过多或循环展开后变量数量过多,可能让每个线程占用更多 register。GPU 同时驻留的线程数量下降后,内存延迟更难被其他线程束覆盖。更糟的情况是 register spill,编译器把本应留在 register 的数据放到较慢的内存路径中。此时源码里看起来只是多了几个变量,工具里却表现成 local memory 或 global memory 访问增加。
shared/local memory 的价值来自“把重复的远距离读取改成一次远距离读取加多次近距离复用”。在 tile-based blur、prefix sum、cluster lighting 或 compute culling 中,多个线程会访问同一小块数据。先把这块数据装入 threadgroup memory,再让组内线程复用,可以减少对 L2/VRAM 的请求。这个策略要求数据块边界清晰、组内访问密集、同步次数受控;若每个线程只读一次,搬入 local memory 反而增加了加载和同步成本。
texture cache 面向图形采样模式。相邻 fragment 往往读取相邻 UV,sampler 还会执行双线性、三线性或各向异性过滤。cache 看到的是一组靠近的 texel 请求,mipmap 让远处表面读取更小层级,从而减少无效高频数据。贯穿 frame 中,远处模型仍读取 4K 法线贴图最高层级时,视觉贡献有限,采样带宽却很高;mipmap 选择和纹理压缩会直接改变这条路径的字节量。
L1/L2 与 VRAM 的关系适合用“命中吸收重复,未命中转成外部请求”理解。G-buffer lighting pass 读取同屏像素的 normal、depth、albedo 时,访问顺序通常比较连续,cache 和内存控制器容易形成有效事务。若 shader 用随机索引访问大型 material buffer,或者每个 fragment 根据噪声读取完全不同的 texture array 层,L2 命中率会下降,显存请求数量会上升。工具中的 memory transaction 数量、L2 hit rate 或 memory stall 能帮助区分这两种情况。
Unified memory 降低了资源复制模型的复杂度,但它不等于访问零成本。Apple Silicon 等统一内存平台让 CPU 和 GPU 共享物理内存,上传路径和资源管理模型会更直接;GPU 仍有自己的 cache、tile memory、同步边界和带宽上限。若 CPU 在同一帧频繁写入 GPU 读取的 buffer,或者 GPU 写完后 CPU 立刻读取结果,等待会转移到同步点。判断统一内存平台性能时,应观察资源生命周期与读写方切换,而不只看是否存在显式 copy。
3.2 缓存命中机制与数据局部性原则
缓存命中来自可预测的数据复用。GPU cache 无法理解“这张贴图是树皮材质”这种语义,它只看到地址、请求大小、访问顺序和时间距离。对图形开发者来说,局部性可以拆成空间局部性、时间局部性和线程束内访问聚合。三者共同决定一次读取能服务多少个 lane、多少个像素或多少次后续计算。
空间局部性指相邻执行单元访问相邻地址。贯穿 frame 中,相邻 fragment 采样同一张 albedo 纹理的相邻 UV,texture cache 可以把一小块 texel 留在片上,后续像素继续命中。buffer 读取也类似,若线程 i 读取 vertices[i] 或 materials[i],内存事务能覆盖多个 lane。若线程 i 读取 materials[randomIndex[i]],请求会分散到很多 cache line,单次事务服务的有效数据减少。
时间局部性指同一数据在短时间内被再次使用。lighting pass 中读取 depth 后计算 view position,随后又用同一个 depth 做 fog 或 screen-space effect,若操作位于同一 pass 或相邻的紧凑路径中,cache 更可能复用。跨很多 pass、跨 queue 或经过 render target resolve 后,上一轮 cache 状态可能已经失效。时间局部性不是“曾经读过”,而是“在 cache 被替换前再次读到”。
coalesced access 是并行执行语境中的局部性。一个 warp 或 wavefront 内的 lane 同时发出 load 时,硬件会尝试把相邻地址合并成少量内存事务。连续地址、对齐地址和一致的访问宽度会提高事务效率;大 stride、非对齐结构、按指针追链和随机索引会让事务分裂。图形管线里的顶点 attribute、instance data、material buffer 和 compute buffer 都会受到这个规则影响。
下面的简化 shader 片段用于说明访问形态。它不绑定某个 API,只表达两个 buffer 读取模式的差异。
// 连续读取:相邻 invocation 读取相邻 material,内存事务更容易聚合。
Material material = materials[globalInvocationId];
// 间接读取:相邻 invocation 可能跳到完全不同的位置。
uint materialIndex = materialIndices[globalInvocationId];
Material materialByIndex = materials[materialIndex];
第一段读取模式更容易命中 cache line,也更容易形成 coalesced transaction。第二段在材质索引已经按空间排序时也可能表现良好;若索引来自随机可见集或未排序的对象列表,cache 会频繁拉入当前线程束只使用一小部分的数据。这里的结论不是“间接索引低效”,而是间接索引需要配套排序、分桶或 compact,让同一批线程访问接近的数据。
cache line 是观察访问浪费的单位。一次 memory transaction 往往会拉入一段连续字节,而 shader 可能只使用其中几个分量。以顶点属性为例,若 position、normal、UV、tangent、color、joint、weight 全部交错在一个大结构里,而当前 depth pre-pass 只需要 position,那么每个顶点读取会带入大量当前 pass 用不到的数据。把只服务 depth 的 position 放入更紧凑的 stream,可以减少 attribute bandwidth。
纹理访问也有类似问题。一个 full-screen pass 若对每个像素读取四张全分辨率纹理,空间局部性通常好,但总字节量仍可能超过带宽预算。cache 命中只能减少重复的外部请求,无法消除必须读取的数据体积。此时优化方向从“提高命中率”转到“减少字节数”:压缩格式、半精度格式、合并通道、降低分辨率、使用 mipmap 或把多次读取合并到同一个 pass。
随机访问的代价在 GPGPU 和 GPU-driven 渲染中更明显。compute culling、cluster light list、particle simulation、visibility buffer 都可能通过索引表访问不连续数据。解决顺序通常是先让数据在生成阶段就按空间或材质排序,再把活跃元素 compact 成连续数组,最后让 shader 以线性方式遍历。若排序成本超过读取收益,应保留原结构并控制随机访问范围,例如用 tile、cluster 或 page 把随机性限制在局部区域。
判断缓存问题时,需要分清“缓存命中率低”和“命中率高但带宽仍满”。前者常见于随机读取、stride 过大和数据布局不匹配;后者常见于 full-screen 后处理、G-buffer lighting、MSAA resolve 和高分辨率纹理采样。贯穿 frame 中,如果打开 4K 贴图后 L2 hit rate 仍然不差,但 memory throughput 接近上限,问题更像总字节量过大;如果 material buffer 小而 L2 miss 升高,问题更像访问顺序破坏了局部性。
3.3 内存带宽与延迟对图形性能的影响
带宽和延迟是两类不同症状。带宽限制表现为单位时间搬运的字节量接近硬件上限,更多 ALU 优化对帧时间帮助有限。延迟限制表现为线程束等待数据返回,执行单元空转,吞吐没有被填满。GPU 通过大量线程和调度切换隐藏延迟,但它无法突破总线、显存、cache、render target 或统一内存的总带宽上限。
texture fetch 的成本由采样次数、格式字节数、过滤方式、mipmap 层级和访问局部性共同决定。贯穿 frame 中,一个像素读取 albedo、normal、roughness、metallic 四类纹理,看起来只是四次采样;若每张纹理是高分辨率、未压缩、三线性过滤,实际请求会扩展到多个 texel 和多个 mip 层。远处物体若没有合理 mipmap,会读取高频 texel 并产生走样,画质和带宽同时受损。
buffer read/write 的瓶颈经常出现在 compute pass 和 GPU-driven 数据路径。compute culling 读取 object bounds,写出 visible list;particle update 读取旧位置和速度,写出新状态;cluster lighting 读取 light list,写出 tile 结果。每次读写都要考虑结构大小、对齐、是否连续、是否存在原子操作和后续 pass 是否立刻读取。写入后的资源若马上作为 draw indirect 或 shader storage buffer 使用,还会引入 barrier 或 queue 依赖。
render target 写入是图形 frame 中稳定存在的带宽来源。G-buffer pass 可能同时写 albedo、normal、roughness、metallic、depth、motion vector。每增加一个 MRT,就增加一个像素写入通道;分辨率、MSAA sample count 和格式位宽会按比例放大写入量。若从 RGBA8 换成 RGBA16F,每像素字节数上升;若从 1x sample 换成 4x MSAA,某些 attachment 的存储和 resolve 成本也会改变。
resolve 和 copy 属于容易被忽略的显式搬运。MSAA render target 需要 resolve 到单采样纹理后给后处理或呈现使用;动态纹理上传可能从 staging buffer copy 到 device-local texture;readback 会把 GPU 结果拷回 CPU 可见内存。它们有时不出现在 shader 代码里,却会出现在 command list、blit encoder、copy pass 或 render pass 结尾。工具中若看到 copy/resolve pass 占用明显时间,应把它纳入带宽预算。
同步等待会把内存问题伪装成“某个 pass 很慢”。例如 compute pass 写 lighting tile list 后,fragment pass 立刻读取;若 barrier 范围过大或资源状态转换覆盖整张大纹理,GPU 需要完成更多写回和可见性保证。另一个例子是 CPU 每帧更新一个 uniform buffer 后 GPU 仍在读取上一帧同一区域,驱动或 runtime 可能插入等待。此时单条 shader 并无明显问题,帧时间上升来自资源生命周期冲突。
可以用一个粗略预算理解 full-screen pass。若 3840×2160 分辨率下,每像素读取四个 RGBA16F 纹理并写一个 RGBA16F render target,单 pass 的理论字节量已经很高;再叠加过滤、多次采样、cache miss、blend、MSAA 或历史帧读取,带宽压力会继续放大。这个估算不追求精确周期数,它用于判断某个 pass 是否天然接近带宽型 workload。
延迟型问题更常见于不规则读取。fragment shader 根据屏幕空间噪声随机访问一张巨大 lookup texture,或 compute shader 根据链表遍历不连续节点,单个线程束可能频繁等待内存返回。若 occupancy 充足,GPU 调度器可以切换到其他线程束;若 register 压力高、shared memory 占用高或 thread group 规模配置不合适,可驻留线程减少,延迟隐藏能力下降。于是同一段代码在小分辨率下还能运行,放大数据后就出现 stall。
视觉症状也能提供线索。贴图分辨率上升导致帧时间近似按字节量增长,通常指向 texture bandwidth。打开 G-buffer attachment 后帧时间增加,通常指向 render target bandwidth 或 resolve。相机快速移动时历史重投影失效,TAA 或 screen-space effect 读取更多无复用数据,可能指向 cache 和 history buffer 访问。材质数量增加导致帧时间抖动,可能指向 material buffer 间接索引和排序问题。
3.4 内存访问优化技巧
内存优化应先选择目标资源,再选择动作。对贯穿 frame 来说,若 bottleneck 在纹理采样,优先检查格式、mipmap、压缩和采样次数;若 bottleneck 在 G-buffer 写入,优先检查 attachment 数量、格式、MSAA 和 pass 组织;若 bottleneck 在 compute buffer,优先检查结构布局、访问顺序、compact 和同步范围。不同瓶颈对应不同资源路径,把所有技巧混用会增加工程复杂度。
结构布局的第一条判断是当前 pass 需要哪些字段。AoS(array of structures)把一个对象的字段放在一起,适合每次都读取完整对象。SoA(structure of arrays)把同类字段放在连续数组中,适合批量读取某几个字段。渲染中常见折中是按 pass 拆 stream:depth pre-pass 只读取 position,lighting 或 material pass 再读取 normal、UV、tangent 和 material id。这样可以让 cache line 中的有效字节比例更高。
连续访问通常比复杂索引更容易优化。对象列表按 material、mesh、texture 或 screen tile 排序后,同一批 draw 或同一批线程会读取接近的资源。GPU-driven pipeline 中,culling 后的 visible object list 应尽量 compact 成连续范围,再用 indirect draw 或 meshlet 批量处理。若每个可见对象保留原始散乱索引,后续 shader 会把随机性带入材质读取、instance data 读取和纹理数组访问。
纹理压缩直接减少字节量,并且常常保持采样路径友好。颜色贴图适合使用 BC/ASTC/ETC 等平台支持的压缩格式;法线、粗糙度和金属度需要选择能保留信号特征的格式和通道打包方式。roughness、metallic、ambient occlusion 常被打包进不同通道,减少 texture binding 和采样次数。这个动作要配合色彩空间和精度检查,防止把线性数据按 sRGB 处理。
mipmap 是远距离采样的带宽和画质工具。没有 mipmap 的高频纹理在远处会产生闪烁,sampler 也需要从高分辨率层读取过多 texel。生成 mipmap 后,远处表面读取更低层级,cache 工作集变小,采样结果更稳定。各向异性过滤会改善斜视角纹理质量,也会增加采样成本;应把它用于地面、墙面、道路等视觉收益明显的材质,而非全局无差别开启最高等级。
tile/local memory 适合把屏幕局部数据聚合处理。比如 deferred lighting 可以按屏幕 tile 先构建 light list,再让 tile 内 fragment 复用;compute blur 可以把输入纹理的一块区域和边界 halo 装入 threadgroup memory,再多次访问。这个方法的判断条件是:同一小块数据会被同一组线程多次读取,局部数据量能放进 threadgroup memory,同步次数可控,边界处理不会让代码复杂度压过收益。
批量上传策略解决 CPU 到 GPU 的资源更新问题。每帧创建和销毁大量小 buffer,或频繁更新分散的 texture region,会增加 API 调用、内存分配、同步和 copy 成本。更稳定的方式是使用 ring buffer、staging buffer、persistent mapped buffer 或统一的 upload heap,把小更新合并成连续写入,再由少量 copy 或状态转换提交给 GPU。多帧并行时,应给动态资源留出 frame-in-flight 空间,减少 CPU 写入与 GPU 读取同一内存区域的冲突。
render target 的优化重点是 attachment、格式和 pass 生命周期。G-buffer 中每个通道都应回答“后续 pass 是否真的读取它”。normal 可以压缩编码,roughness/metallic 可以合并,motion vector 可按需要开启,HDR 格式可按亮度范围选择。tile-based GPU 上,合理使用 load/store action、transient attachment 或 memoryless attachment 能减少外部内存写回;immediate-mode GPU 上,减少 full-screen attachment 数量和 resolve 次数通常更直接。
同步优化要把 barrier 范围缩小到真实依赖。若 compute pass 只写一个 visible list,后续 draw 只需要 indirect argument 可见,就不应把整组无关 texture 和 buffer 都纳入同一大 barrier。若一个 render target 后续只作为 shader read 使用,应让 state transition 清楚表达写后读依赖。过宽的同步会让 GPU 放弃部分并行和 cache 复用,过窄的同步会产生数据可见性错误;正确做法是按资源、子资源、访问类型和管线阶段描述依赖。
可以把优化流程固定成四步。第一步,记录目标 frame 中最慢 pass 和资源读写表。第二步,按 texture、buffer、render target、copy/resolve、sync 分类估算字节量和访问形态。第三步,用工具观察 bandwidth、cache、stall 或 pass time 证据。第四步,只对证据指向的路径修改布局、格式、mipmap、local memory、上传或同步。每次只改一类路径,才能把帧时间变化和动作建立因果关系。
3.5 工具与分析:使用性能分析器观察内存行为
性能分析器的作用是把“感觉慢”转成可定位证据。RenderDoc、Nsight Graphics、Xcode GPU tools 的具体 counter 名称和可用范围不同,但观察目标相同:当前 frame 哪个 pass 占用时间,哪个资源被大量读取或写入,cache 是否命中,memory transaction 是否异常,GPU stall 是否来自内存等待或同步。工具给出材料,结论需要结合 pipeline 和资源生命周期判断。
RenderDoc 更适合作为 frame 结构入口。它能帮助查看 draw call、pipeline state、resource binding、texture、buffer、render target 和 pass 顺序。面对贯穿 frame,先在 capture 中定位 G-buffer pass、lighting pass、post-process pass 和 resolve/copy 操作,再查看每个 draw 或 dispatch 绑定了哪些 texture、sampler、buffer 和 attachment。即使没有完整硬件 counter,也能确认“这个 pass 到底读写了什么”。
Nsight Graphics 更适合 NVIDIA 平台上的 GPU counter 和 workload 分析。使用它观察 memory workload 时,应把指标放回具体 pass:memory throughput 接近上限说明带宽压力大,L2 hit rate 下降说明请求频繁落到更远层级,warp stall memory dependency 上升说明线程束等待数据,texture unit 或 framebuffer 输出相关指标上升说明采样或 render target 路径可能占主导。指标名称会随版本和 GPU 变化,判断维度应保持稳定。
Xcode GPU tools 更适合 Apple 平台上的 Metal frame、tile、bandwidth 和 shader 观察。统一内存和 tile-based 架构下,外部 memory bandwidth、tile memory、load/store action、render pass attachment 生命周期会影响结果。若一个 render pass 的 attachment 在 tile 内完成后无需后续读取,应尽量让 store 行为表达这个事实;若必须跨 pass 读取,就要接受写回和后续读取成本。工具观察应围绕 render pass 边界和资源读写方切换展开。
下面的分析路径适用于本章贯穿 frame,也适用于大多数图形工程排查:
- 先按 frame capture 找到 GPU 时间最高的 pass,并记录它是 draw、dispatch、copy 还是 resolve。
- 列出该 pass 的输入 texture、buffer、sampler、uniform,以及输出 render target 或 storage buffer。
- 判断每个资源的字节规模:分辨率、格式、sample count、mipmap 层级、结构大小、元素数量。
- 判断访问模式:连续、按屏幕相邻、按对象排序、间接索引、随机、读后写、写后读。
- 对照 counter:bandwidth 高看字节量,cache miss 高看局部性,stall 高看延迟隐藏和同步,copy/resolve 高看资源生命周期。
- 只修改一条假设路径,并用同一 camera、同一分辨率、同一资源集再次捕获。
工具证据需要和视觉结果一起看。把 albedo 从未压缩 RGBA8 改成合适压缩格式后,memory throughput 下降但贴图出现明显色带,说明格式选择影响质量。给 normal map 生成 mipmap 后,远处闪烁下降且采样成本下降,说明 mipmap 同时改善了采样稳定性和工作集大小。把 G-buffer normal 从 RGBA16F 改成编码格式后,带宽下降但边缘法线出现误差,需要在画质和性能之间重新选格式。
分析时要排除 CPU 提交瓶颈和 shader ALU 瓶颈。若 GPU pass time 很低但 CPU frame time 高,memory counter 的变化对总帧率贡献有限。若 shader ALU utilization 高、memory throughput 不高、cache 指标稳定,问题更可能来自复杂光照、BRDF、循环或分支。若 copy/resolve pass 明显占时,优化 shader 代码也难以改善这段搬运。先区分瓶颈类型,后续动作才有意义。
跨平台比较要保留边界。独显 VRAM 平台上,纹理和 render target 带宽可能是主要约束;移动或 Apple Silicon 平台上,tile memory、统一内存、功耗和热限制会改变最优策略;某些 API 后端还会自动压缩 render target 或重排资源布局。工具中看到的 counter 是当前平台、当前驱动、当前 frame 的证据,不能直接推广成所有 GPU 的规律。
本章建立的最终判断顺序是:先画出资源读写路径,再判断局部性和字节量,随后用工具验证 bandwidth、cache、stall 和同步证据,最后选择针对单一路径的优化动作。这样处理后,“内存层级”从抽象硬件名词变成可复查的 frame 分析方法。
最小自检任务
给定一个 1440p 的 deferred frame:G-buffer pass 写入 albedo RGBA8、normal RGBA16F、roughness/metallic RGBA8、depth;lighting pass 全屏读取这些 attachment,并额外采样一张 4K normal map;post-process pass 做 tone mapping;开启 4x MSAA 后 GPU 时间明显上升。请判断这个 frame 中最可能优先检查的三条内存路径,并说明每条路径要看什么证据、如何提出一个最小优化动作。
答案要点
第一条路径是 G-buffer 与 MSAA attachment 写入。4x MSAA 会放大部分 attachment 的存储和 resolve 成本,应先在 frame capture 中查看 G-buffer pass、resolve 或 store 操作的 GPU 时间,并记录 attachment 格式、sample count 和后续读取关系。最小优化动作是检查 normal 是否需要 RGBA16F,roughness/metallic 是否已经合并,某些 attachment 是否可以降低精度或只在需要的 pass 开启。
第二条路径是 lighting pass 的全屏读取。lighting pass 每个像素读取多个 G-buffer attachment,分辨率固定,访问通常连续;若 memory throughput 高而 cache 指标稳定,说明总字节量可能占主导。最小优化动作是减少读取通道、压缩 normal encoding、合并材质通道,或者把只服务局部效果的读取移到更小分辨率或受限区域。
第三条路径是 4K normal map 的纹理采样。应检查采样次数、mipmap 是否存在、过滤方式、远处模型是否读取过高层级,以及 texture cache 或 texture unit 相关指标。最小优化动作是生成 mipmap、选择合适压缩格式、限制高等级各向异性过滤使用范围,并在同一 camera 下比较画质和 memory throughput。
必要边界是先确认瓶颈确实在 GPU 内存路径。若 CPU frame time 主导,或工具显示 ALU utilization 高且 memory throughput 不高,以上三条内存优化不会成为优先动作。若 copy/resolve pass 单独占用大量时间,应把 resolve 本身作为独立路径检查,fragment shader 修改只能作为次级动作。
本章知识点总结
- 成本地图:GPU 内存层级应按距离执行单元、可复用性、带宽和同步边界一起判断。
- Register 压力:临时变量过多会降低 occupancy,spill 会把局部计算转成更慢的内存路径。
- Local 复用:shared/local memory 适合同一 thread group 多次读取同一小块数据的场景。
- Texture cache:纹理缓存依赖二维空间局部性、mipmap 选择、过滤方式和采样顺序。
- Cache line:一次内存事务会拉入连续字节,结构布局决定有效字节比例。
- 连续访问:相邻线程访问相邻地址时,coalesced access 能减少事务分裂。
- 随机访问:间接索引需要排序、分桶或 compact,才能把请求限制在局部范围。
- 带宽瓶颈:高分辨率纹理、G-buffer、MSAA、resolve 和 full-screen pass 会快速放大总字节量。
- 延迟瓶颈:不规则读取与低 occupancy 组合时,线程束更容易等待数据返回。
- 格式选择:压缩格式、半精度格式和通道打包会改变字节量,也会改变画质边界。
- Mipmap 作用:mipmap 同时减少远距离采样工作集并降低纹理闪烁。
- 上传策略:ring buffer、staging buffer 和批量 copy 可以减少小资源更新带来的同步和提交成本。
- 同步范围:barrier 应按真实资源、访问类型和管线阶段描述依赖,过宽会降低并行度。
- 工具证据:RenderDoc 适合定位资源绑定,Nsight 和 Xcode GPU tools 适合观察平台相关 counter。
- 分析顺序:先记录资源读写路径,再判断局部性和字节量,最后用 counter 验证并修改单一路径。