GPU Gems 2 · Chapter 46. Improved GPU Sorting

从 compare-swap、odd-even merge 到 bitonic merge,理解如何用双缓冲、插值比较参数和 key/index packing 让 GPU 排序充分利用 fragment、vertex 与 rasterizer。

学习目标

  • 能解释 data-independent sorting 为什么适合 GPU,并用 compare-swap 描述一个排序网络 pass
  • 能比较 odd-even merge 和 bitonic merge 的 pass 数量、中间状态和实时响应取舍
  • 能把排序 pass 映射到双缓冲 fragment program,并说明 vertex/rasterizer 如何提供比较方向与 partner 地址
  • 能通过实验推进排序网络,确认 key 与 index 成对交换,并评估 packing 对 fragment 数量的影响

排序是透明物体可见性、early-z 利用、粒子距离和碰撞空间结构的共同基础。CPU 上可以自由读写数组,但 GPU 更像大量同步执行的 SIMD lane:算法必须事先规定每个位置在每个 pass 比较谁、把结果写到哪里。

本章从简单的 odd-even transition network 出发,再转向 odd-even merge 和 bitonic merge。优化的重点不是减少某一条比较,而是让 fragment、vertex processor 和 rasterizer 都参与工作,并把 key/index 记录压紧到更少的 fetch 和 fragment 中。

1. data-independent sorting 和 compare-swap

不根据当前 key 选择下一步算法路径;同样数量的 item、stage 和 pass 总会执行同样的比较网络。这种确定结构很适合 GPU,因为线程无需互相等待或动态分配工作。

的一个基本单元接收两个 key,把较小值送到升序通道,把较大值送到降序通道。多个单元排成一列就是一个 pass;输入从左向右经过多列后逐渐有序。

排序网络:compare-swap 是 GPU 的基本积木abcomparekey a ? key bmin → 上通道max → 下通道排序网络不依赖数据分支路径,控制流由 pass 参数提前确定
一个 compare-swap 单元只读两个位置、写回两个确定位置;许多单元可以并行。

最直接的 odd-even transition sort 每轮只比较相邻位置。最坏情况下最小值每次只移动一步,因此要用 O(n²) 的 pass 才能完成 n 个元素的排序;单个 pass 很便宜,但总 pass 数在实时场景里太昂贵。

2. odd-even merge:减少全排序 pass

把排序分成 stage 和 pass。排序 m 个元素的 stage 需要 log(m) 个 pass,所有 n 个元素总体是 O(n log²(n) + log(n)) 的 pass 结构。它仍然比 CPU 常见的 O(n log(n)) 多一些,但比逐步相邻交换少得多。

两种网络:可铺开,但优化目标不同odd-even merge中间状态较平滑bitonic merge升降序 bitonic 序列实时粒度优先 odd-even;吞吐优先 bitonic
两种网络都适合 GPU,但中间状态和 pass 数量的取舍不同。

原章的 GPU 实现用 CPU 上的两个循环只传 stage、pass 和派生 uniform,然后绘制 full-size quad。fragment shader 读取当前位置和 partner 位置,执行 less-than 或 greater-equal,并写到 target buffer。问题是有些值只被复制、不需要比较,但复制和比较仍然消耗同样的 fragment 时间。

3. bitonic merge:让每个 fragment 都有工作

让每个 stage 产生一组升序和降序子序列,然后逐步合并。第 i 个 stage 有 i 个 pass;比较距离由 stage 和 pass 决定,能够用位操作、取模和符号方向表达。

bitonic merge:stage × pass 的规则网格stage 1pass 1d=1stage 2pass 1d=2pass 2d=1stage 3pass 1d=4pass 2d=2pass 3d=1CPU 只改变 stage、pass、compare distance;shader 每轮做同一类工作
第 i 个 stage 有 i 个 pass;比较距离从远到近,最终合成全局有序序列。

直接在 fragment 中计算所有 modulo、位移和 partner 地址并不一定高效。更好的做法是把一组具有相同参数的 fragment 组成 quad:vertex program 只给 quad 两侧一个 +1−1 的 flag,rasterizer 线性插值它,fragment shader 根据 flag 的符号选择比较方向。这样 vertex processor 和 rasterizer 分担了原本由 fragment 重复完成的工作。

一个 GPU pass:全屏 quad + 双缓冲source texturekey[i]key[partner(i)]只读fragment shadercompare + directionmin / maxwrite resulttargetnext交换下一轮把 target 当 source;排序直到所有 stage/pass 完成
每个 pass 使用两张纹理隔离读写;全屏 quad 覆盖所有待排序位置。

对于二维 field,行 pass 使用水平 compare distance,列 pass 则转置 quad 并使用垂直距离。两种方向共享同一个 sorting network 逻辑,但 texture 坐标必须分别保证 partner 位于正确的行或列。

4. key/index packing:减少 fetch,同时保留关联

是粒子排序和透明物体排序的关键。key 可以是 viewer distance,index 则指向粒子或几何对象;交换时二者必须一起交换。

key/index packing:排序键不能和对象索引分离两个逻辑 itemkey 0.42index 17key 0.77index 04一个 RGBA texelRkey 0.42Gindex 17Bkey 0.77Aindex 04最后一级只比较邻居:同一 fragment 内完成,省掉第二次 texture fetch
把 key 和 index 作为一体移动,既保持排序对象关联,也降低最后一级比较的取样次数。

GPU 使用四分量向量,因此可以把两个 key/index pair 装到一个 RGBA texel,切掉一半 row width。bitonic 每个 stage 的最后一级只比较相邻 item,这时两个 item 已经在同一个 fragment 中,可以把比较折叠到一次 shader 调用并省掉第二次 texture fetch。

// 逻辑上是两个 key/index pair;实际由一个 RGBA texel 携带
float4 packed = tex2D(source, texcoord);
Pair left  = unpackPair(packed.xy);
Pair right = unpackPair(packed.zw);
Pair small = left.key < right.key ? left : right;
Pair large = left.key < right.key ? right : left;
return packPairForDirection(small, large, direction);

这里的 direction 由 pass 的升序/降序区域决定,而不是由输入数据动态决定。pack 之后要测试 n 不是 2 的倍数时的尾部、index 精度和 key 的排序稳定性。

5. 动手实验:逐 pass 观察排序网络

先预测:bitonic 的某个 stage 尚未完成时,序列会不会已经全局有序?打开 packing 后,比较次数会变少,还是只会减少需要处理的 fragment groups?

GPU Gems 2 · Chapter 46

排序网络:逐 pass 看 key 和 index 如何交换

选择 bitonic 或 odd-even,推进比较 pass,观察真实 key/index 数组、比较次数和 packing 后的 fragment 数。

▷ 可交互
当前序列 · 初始输入00.6210.1820.9130.3440.7750.0860.5370.4580.2990.86100.11110.70120.39130.99140.24150.57比较伙伴pass 0/10 · comparisons 0 · fragment groups 16 · 1 个 key/index 对 / fragment
排序键和 index 始终成对移动;真正使用粒子系统时,index 会继续指向位置、颜色或生命周期等 payload。

实验执行真实的 bitonic 与 odd-even pass:每次推进都会生成新的 key/index 数组,比较次数按实际比较单元累加;packing 只改变逻辑 item 到 fragment group 的映射,不改变排序结果。黄色柱表示当前 pass 触及的 partner,编号表示 index 仍然跟着 key 移动。

6. 实时应用的取舍

odd-even merge 的优势是中间结果更适合逐帧观察,排序过程可以更自然地分摊到多帧;bitonic merge 的优势是更积极地重排并更充分地使用 GPU 资源,但每个 stage 的中间状态并不保持全局有序,不能随意当成“已经可显示的最终排序”。

GPU 的 pass 数仍然要结合 field 大小、纹理带宽和目标设备测量。原章报告了在 GeForce 6800 Ultra 上,bitonic 对 256²、512²、1024² field 的 pass 数和吞吐;这些数字是历史硬件实验,不应直接当作现代设备的基准。应保留算法结构,重新测量实际平台。

粒子按 camera distance 排序时,短暂的小误差有时比让应用完全停顿更可接受;但透明混合、阴影或严格碰撞排序可能需要完整有序结果。排序算法的选择必须跟视觉/物理容错契约一起做,而不是只看“GPU 比 CPU 快”。

小结

  • data-independent sorting 用固定网络把比较路径交给 GPU 并行执行。
  • odd-even transition 简单但 pass 太多,odd-even merge 通过 stage 降低总成本。
  • bitonic merge 让每个 pass 更规则,并利用 vertex/rasterizer 生成方向和地址参数。
  • 双缓冲隔离 source/target,key/index pair packing 减少 fetch 但必须保留关联。
  • bitonic 与 odd-even 的中间状态和响应特性不同,应按实时容错选择。

练习

问题 1|网络选择 一个粒子系统允许中间帧暂时近似有序,但要求每帧响应;一个透明渲染提交要求整批排序完成。分别更倾向 odd-even 还是 bitonic?为什么?

问题 2|改写 Demo 把实验的 key 从固定数组改成 camera distance,并把 index 用作粒子 ID。为什么交换 key 时必须同步交换 index?packing 的尾部要检查什么?

问题 3|性能排查 bitonic shader 看起来比 odd-even 复杂,但实际更快时,应该观察哪些证据,而不是只比较源码行数?

名词解释

本章出现的专业名词,用大白话再讲一遍。

data-independent sorting
比较路径只由位置和 pass 参数决定、不由输入 key 动态改变的排序方式。
sorting network
由固定 compare-swap 连接组成、每列可以并行执行的排序结构。
odd-even merge sort
用奇偶交错规则合并有序子序列的固定排序网络。
bitonic merge sort
先构造升降序交替的 bitonic 序列,再按比较距离逐步合并的排序网络。
key/index pair
绑定在一起移动的排序键和对象索引记录。
double buffering
用 source 和 target 两个缓冲轮流读写、避免同一纹理原地读写的方法。

资料与写作方式声明

本章以GPU Gems 系列权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…