共享内存是 GPU 片上最快的显存之外的存储,但它快是有前提的:访存模式不对,再快的片上存储也会被拖成串行。理解 bank 冲突,是优化 CUDA 内核访存的第一课。

共享内存通常被划分为 32 个 bank,每个 bank 宽度为 4 字节,相邻地址轮流落在不同 bank 上。一个 warp 的 32 个线程如果在同一条访存指令中访问 32 个不同 bank,硬件可以在一个周期内并行完成;如果多个线程命中同一个 bank,而地址又不同,请求就必须排队,这就是 bank 冲突。冲突度为 N,带宽就近似降为原来的 1/N。唯一例外是多个线程读同一个地址,硬件会广播,一次即可满足。

最典型的反模式是按列访问二维数组。假设线程按行号连续编号,却访问同一列的不同行,地址步长恰好是行宽的倍数,32 个线程就可能全部落到少数几个 bank 上。把访存改成按行连续,或者先做转置再访问,冲突立刻消失,实测带宽翻倍并不稀奇。改的不是算法复杂度,只是数据布局与线程到数据的映射关系。

排查思路很直接。先用性能分析工具看共享内存的冲突计数与重放次数,确认瓶颈确实在共享内存而非全局内存或计算。再看访存模式:步长、行宽、数据宽度是不是 32 的倍数。常见修法有三种:一是填充,在每行末尾多留一列,打破步长与 bank 数的整除关系;二是重排,把数据按访问顺序预先转置;三是交换索引,让连续线程访问连续地址。填充会浪费少量空间,但换来并行度,通常很划算。

还有两个细节容易被忽视。第一,8 字节或 16 字节的向量访存会一次占用两个或四个 bank,设计时要把向量宽度算进去。第二,冲突只影响共享内存这一级,改完之后要重新测全局带宽与整体耗时,避免在错误的地方反复优化。共享内存优化的本质,是让 32 个线程的访存请求在硬件层面真正并行,而不是看起来并行。一句话总结:共享内存的带宽不是被硬件上限卡住,而是被访存模式浪费掉的,改对映射关系,性能提升往往立竿见影。