GPU 的思维方式:并行、分支代价与访存
CPU 思维写 shader 会写出「能跑但很慢」的代码。GPU 的执行模型是「海量像素同一时刻做同一件事」,理解这一点,才能预判什么写法贵、贵在哪。这篇讲 GPU 怎么执行你的 shader,以及由此得出的几条硬规则。
SIMT:以「一组像素」为单位执行
GPU 把片元按组(NVIDIA 叫 warp,32 个像素;AMD 叫 wavefront,32 或 64 个)打包,同一时刻整组执行同一条指令——所谓 SIMT(单指令多线程)。你不是在给一个像素写程序,是在给「一组像素」写程序,这决定了后面所有规则。
分支为什么贵:分歧(divergence)
组内像素条件不一致时(一半像素走 if、一半走 else),硬件的处理是两条路径都执行,只是用掩码让输不起的像素不写入结果。也就是说:
// 组内条件不一致时:两条分支的代价都要付
if (uv.x > 0.5) c = SampleA(uv);
else c = SampleB(uv);
由此得出硬规则:
- 片元里的动态 if 不免费,分支两侧越重越贵。能用
lerp、step、saturate数学表达的,优先数学表达。 - uniform 分支几乎免费:整组条件一致(如材质开关、按 Draw Call 传入的参数),硬件一次判定全组同路。这就是 keyword/变体比动态分支快的原理——把「材质级开关」编译成独立代码,见 /notes/unity/engineering-3/。
- 循环同理:次数不一致的循环按最大次数执行;编译期可知次数的展开循环基本免费。
采样、ALU、访存:谁更贵
一个片元程序的开销大头通常不是算术,而是等数据:
| 操作 | 贵在哪 | 直觉 |
|---|---|---|
| 纹理采样 | 显存带宽 + 缓存命中率;相邻像素采样位置连续则命中好 | 移动端带宽是头号瓶颈,见 /notes/unity/performance-1/ |
| 依赖采样 | 第二次采样的 UV 要等第一次采样算完,流水线停顿 | 「采样套采样」比两次独立采样贵 |
| ALU(乘加、点积) | 现代 GPU ALU 吞吐极高 | 相对便宜,别为省两次乘法牺牲可读性 |
| 插值器读取 | 每个片元读 v2f 数据,数量有上限 | 少传能重算的数据 |
推论:「用计算换采样」是常见优化方向(如程序化噪声代替噪声贴图),但换过头会变成 ALU 瓶颈——两头都要测,测量方法见 /notes/unity/performance-2/。
并行还意味着:没有共享记忆
一个片元的执行与其他片元完全无关——不能读邻居像素的颜色(屏幕空间效果要靠深度/法线贴图或拷贝,见 /notes/unity/effects-4/),不能跨像素通信。这也是为什么边缘检测必须走「先渲染到纹理、再全屏后处理」的两步结构,见 /notes/unity/effects-3/。
精度也是执行模型的一部分
移动端 GPU 的 half(fp16)吞吐常是 float(fp32)的两倍,但 half 的数值范围只有 ±65504、精度约 3 位十进制——大坐标(世界位置)用 half 会直接错。编译器会自动选精度,自动选的不一定对,坑与规则见 /notes/unity/performance-3/。
自检三问
- 一个全屏 quad,
if (screenPos.y > 0.5)的分支和if (_Intensity > 0)的分支,哪个几乎免费?为什么? - 为什么「先采一张图拿 UV、再按这个 UV 采第二张」比两次独立采样慢?
- 想把噪声贴图换成程序化噪声,判断划不划算要看什么?