GPU内存层次结构:全局内存、共享内存、寄存器、本地内存、常量内存、纹理内存的特性与使用场景

说到GPU的内存,我刚开始接触CUDA时,最头疼的就是搞不清这些内存到底怎么用。你想想看,一个核函数里能访问的内存类型就有六种,每种的速度、容量、作用范围都不一样。搞混了,性能直接掉一个数量级。

今天我就把这六种内存掰开揉碎了讲清楚。我会结合自己踩过的坑,告诉你每种内存到底该怎么用。

全局内存(Global Memory)

全局内存是GPU里容量最大的存储空间。显存有多大,全局内存就有多大。所有线程都能访问它,CPU也能通过cudaMemcpy读写它。

特性:

  • 容量大:通常4GB到80GB不等
  • 延迟高:访问延迟约400-800个时钟周期
  • 所有线程可见:包括CPU端
  • 没有缓存(准确说,L2缓存会加速,但第一次访问还是慢)

使用场景:

  • 存储输入数据和最终结果
  • 线程间需要共享的大规模数据
  • CPU与GPU之间的数据交换
避坑指南:我曾经在项目里把全局内存当共享内存用,频繁读写同一个地址。结果性能惨不忍睹。记住,全局内存的带宽虽然高,但延迟也高。尽量减少访问次数,尽量让访问模式满足合并访问条件。

共享内存(Shared Memory)

共享内存是GPU里的一块"宝地"。它位于芯片内部,延迟极低。同一个线程块内的所有线程都能访问它。

特性:

  • 容量小:通常每个SM只有48KB到164KB
  • 延迟低:约20-30个时钟周期
  • 线程块内共享:不同块之间不可见
  • 需要手动管理:没有自动缓存

使用场景:

  • 线程块内数据重用
  • 规约操作(求和、求最大值等)
  • 矩阵分块计算

我的经验:共享内存用得好,性能翻倍不是梦。我做过一个矩阵乘法优化,用共享内存做分块,性能从200 GFLOPS直接飙到1.2 TFLOPS。但要注意bank conflict,这玩意儿会让你的共享内存访问速度打折扣。

寄存器(Register)

寄存器是GPU里最快的内存。每个线程独享,编译器自动分配。说白了,你在核函数里定义的局部变量,只要不是数组,大概率都放在寄存器里。

特性:

  • 速度最快:0延迟
  • 容量极小:每个线程最多255个寄存器
  • 线程私有:其他线程看不到
  • 编译器自动管理

使用场景:

  • 循环计数器
  • 临时计算结果
  • 频繁访问的变量

小技巧:我习惯在核函数开头就把全局内存的数据加载到寄存器里。比如:float val = input[idx]; 然后后面所有计算都用val,而不是反复读全局内存。这样能省下不少带宽。

本地内存(Local Memory)

本地内存这个名字容易让人误解。它其实不是"本地"的,而是存在全局内存里的。只是每个线程私有的那部分全局内存,被叫做本地内存。

特性:

  • 速度慢:和全局内存一样慢
  • 线程私有:其他线程访问不到
  • 编译器自动使用:当寄存器不够用时,变量会被"溢出"到本地内存

使用场景:

  • 大型局部数组(比如 int arr[100])
  • 寄存器溢出时的后备存储
注意:我曾经写过一个核函数,里面定义了一个 float data[64] 的数组。结果编译器把它放到了本地内存,性能直接掉了30%。后来我改成用共享内存,问题就解决了。所以,尽量避免在核函数里定义大数组,除非你明确知道它在哪。

常量内存(Constant Memory)

常量内存是GPU里一块特殊的只读内存。它有专门的缓存,叫常量缓存。当所有线程访问同一个地址时,效率极高。

特性:

  • 只读:CPU端写入,GPU端只读
  • 容量小:通常64KB
  • 有缓存:常量缓存延迟低
  • 广播机制:同一warp内访问相同地址,只需一次内存事务

使用场景:

  • 存储不会改变的参数(如滤波器系数、查找表)
  • 所有线程都访问相同的数据

我的经验:做图像滤波时,我把3x3的卷积核放在常量内存里。所有线程都读同一个核,常量缓存的广播机制让性能提升了不少。但要注意,如果每个线程访问不同的地址,常量缓存反而会成为瓶颈。

纹理内存(Texture Memory)

纹理内存最初是为图形渲染设计的。它有专门的纹理缓存,支持硬件插值和边界处理。对于有空间局部性的访问模式,纹理内存表现很好。

特性:

  • 只读:通过纹理对象访问
  • 有缓存:纹理缓存针对2D空间局部性优化
  • 支持硬件插值:线性插值、双线性插值等
  • 支持边界处理:自动处理越界访问

使用场景:

  • 图像处理(滤波、缩放、旋转)
  • 科学计算中的网格数据
  • 需要硬件插值的场景

小技巧:纹理内存的缓存对2D访问模式特别友好。如果你的数据访问有空间局部性(比如图像处理中的邻域操作),用纹理内存往往比全局内存快。我做过一个图像高斯模糊,用纹理内存比全局内存快了40%。

内存层次结构总结

下面这张图展示了GPU内存的层次结构。你可以看到,越靠近计算单元,速度越快,容量越小。

GPU内存层次结构 寄存器 最快 · 线程私有 · 编译器管理 共享内存 快 · 线程块内共享 · 手动管理 常量内存 / 纹理内存 只读 · 有缓存 · 广播/空间局部性优化 全局内存 慢 · 所有线程可见 · 容量最大 本地内存(实际在全局内存中) 速度↑ 容量↑ 容量↓ 速度↓

如何选择合适的内存?

我个人的选择原则很简单:

  1. 能用寄存器,绝不用别的:局部变量尽量用寄存器。如果编译器提示寄存器溢出,再考虑其他方案。
  2. 数据重用,用共享内存:同一个线程块内,多个线程需要反复读同一批数据,果断上共享内存。
  3. 只读且所有线程访问相同地址,用常量内存:比如卷积核、查找表。
  4. 2D空间局部性访问,用纹理内存:图像处理、网格计算首选。
  5. 其他情况,用全局内存:但要注意合并访问,尽量让连续线程访问连续地址。
  6. 避免本地内存:除非你明确知道自己在做什么。

核心原则:数据离计算单元越近,访问越快。但容量也越小。你要做的,就是在容量和速度之间找到平衡点。说白了,就是把最常用的数据放在最快的内存里,不常用的数据放在慢一点的内存里。

嗯,GPU内存这块就讲到这里。每种内存都有它的脾气,摸透了,性能优化就成功了一半。我当年刚学CUDA时,就是靠这张内存层次结构图,一步步把性能从"能跑"优化到"飞起"。

可可精品资料库