第三章:CUDA内存优化——全局内存合并访问、共享内存bank conflict、常量内存与纹理内存、统一内存使用技巧

内存优化,说白了就是让数据在GPU里跑得顺畅。我见过太多人把精力全放在计算上,结果性能瓶颈卡在内存访问上。嗯,今天咱们就把这块硬骨头啃下来。

3.1 全局内存合并访问

全局内存是GPU里最大的存储空间,但也是最慢的。为什么慢?因为它离计算单元远。我个人习惯把全局内存想象成一个巨大的仓库,每次取货都要花时间。

合并访问的核心思想:让同一个warp里的32个线程,访问连续的内存地址。这样硬件可以把多次访问合并成一次大的传输。

关键点:合并访问要求对齐到128字节。说白了,就是起始地址得是128的倍数。

来看个例子。假设我们要处理一个float数组:

// ❌ 糟糕的访问模式:按列访问
for (int i = 0; i < N; i++) {
    float val = data[threadIdx.x + i * blockDim.x];  // 跨步访问
}

// ✅ 好的访问模式:按行访问
for (int i = 0; i < N; i++) {
    float val = data[i + threadIdx.x * N];  // 连续访问
}

我在项目中遇到过一位同事,他写了个矩阵转置,性能惨不忍睹。一查,就是按列访问全局内存。改成按行访问后,速度提升了将近10倍。你想想看,就改了个索引顺序,效果天差地别。

小技巧:使用cudaMemcpy时,尽量保证数据在主机端和设备端都是对齐的。我一般用cudaMallocPitch来分配二维数组,它能自动处理对齐。

3.2 共享内存bank conflict

共享内存是片上的,速度比全局内存快两个数量级。但它有个坑——bank conflict。

共享内存被分成32个bank,每个bank宽度4字节。如果同一个warp里的多个线程访问同一个bank的不同地址,就会发生冲突。冲突会导致访问串行化,性能下降。

为什么会这样? 硬件设计上,每个bank每个周期只能服务一个请求。如果两个线程撞到同一个bank,就得排队。

访问模式 是否冲突 性能影响
所有线程访问不同bank 无冲突 最佳
多个线程访问同一bank的不同地址 冲突 串行化
所有线程访问同一bank的同一地址 广播 无冲突(硬件支持广播)

我曾经调试过一个卷积核,共享内存访问模式没设计好,导致bank conflict严重。后来我用了padding技巧——在共享内存数组的每一行末尾加几个空元素,把数据错开。就这么简单的一招,性能提升了30%。

// 避免bank conflict的padding技巧
__shared__ float s_data[32][32 + 1];  // 每行多一个元素

注意:padding会增加共享内存使用量,别加太多。我一般每行加1-4个元素就够了。

3.3 常量内存与纹理内存

这两个是GPU里的特殊存储区域,用好了能省不少事。

常量内存:容量小(64KB),但有专门的缓存。适合存储不会变的数据,比如滤波器系数、查找表。所有线程访问同一个地址时,常量内存最快。

我个人习惯把模型参数放在常量内存里。记得有一次做图像处理,把3x3的卷积核放常量内存,比放全局内存快了将近2倍。

__constant__ float kernel[9];  // 声明常量内存

// 主机端复制
cudaMemcpyToSymbol(kernel, host_kernel, 9 * sizeof(float));

纹理内存:有硬件插值功能,适合图像处理。它支持二维和三维的局部性访问,而且对非对齐访问很友好。

嗯,这里要注意:纹理内存的延迟比共享内存高,但比全局内存低。如果你做的是图像滤波、插值这类操作,纹理内存是首选。

避坑指南:我曾经在纹理内存上吃过亏——忘记设置归一化坐标。结果纹理坐标范围搞错了,图像显示全乱套。记得检查texture对象的参数设置。

3.4 统一内存使用技巧

统一内存(Unified Memory)是CUDA 6.0引入的,它让CPU和GPU共享同一个地址空间。说白了,你不需要手动管理数据迁移了。

但别高兴太早。统一内存不是银弹,用不好反而更慢。

什么时候用统一内存?

  • 代码原型开发阶段,快速验证想法
  • 数据访问模式复杂,手动管理内存太麻烦
  • 处理不规则数据结构(比如链表、树)

什么时候别用?

  • 性能敏感的核心循环
  • 数据访问模式很规律(这时候手动管理更快)
  • 内存容量紧张(统一内存有额外开销)
// 统一内存分配
float *data;
cudaMallocManaged(&data, N * sizeof(float));

// CPU和GPU都能直接访问
// 系统会自动迁移数据

我有个项目,数据是稀疏图结构,用统一内存写起来特别爽。但后来发现性能瓶颈在页面迁移上。解决方案是:用cudaMemPrefetchAsync提前把数据迁移到GPU,避免运行时缺页中断。

核心技巧:使用cudaMemAdvise设置内存访问建议,告诉系统哪些数据会被频繁访问。这能减少不必要的页面迁移。

你想想看,统一内存就像自动挡汽车——市区开很方便,但赛道日还是手动挡更快。选哪个,取决于你的场景。

CUDA内存层次与优化策略 全局内存 大容量,高延迟 合并访问是关键 共享内存 片上,低延迟 避免bank conflict 常量内存 只读,有缓存 适合广播访问 纹理内存 硬件插值支持 适合图像处理 统一内存 CPU/GPU共享 自动数据迁移 优化策略 对齐、合并、padding 预取、建议 延迟从低到高:共享内存 < 常量内存 < 纹理内存 < 全局内存 容量从大到小:全局内存 > 统一内存 > 纹理内存 > 共享内存 > 常量内存

好了,这一章的内容就这些。记住:内存优化的核心是理解硬件的访问模式,然后让你的代码去适应它。别跟硬件较劲,顺着它来,性能自然就上去了。