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内存的层次结构。你可以看到,越靠近计算单元,速度越快,容量越小。
如何选择合适的内存?
我个人的选择原则很简单:
- 能用寄存器,绝不用别的:局部变量尽量用寄存器。如果编译器提示寄存器溢出,再考虑其他方案。
- 数据重用,用共享内存:同一个线程块内,多个线程需要反复读同一批数据,果断上共享内存。
- 只读且所有线程访问相同地址,用常量内存:比如卷积核、查找表。
- 2D空间局部性访问,用纹理内存:图像处理、网格计算首选。
- 其他情况,用全局内存:但要注意合并访问,尽量让连续线程访问连续地址。
- 避免本地内存:除非你明确知道自己在做什么。
核心原则:数据离计算单元越近,访问越快。但容量也越小。你要做的,就是在容量和速度之间找到平衡点。说白了,就是把最常用的数据放在最快的内存里,不常用的数据放在慢一点的内存里。
嗯,GPU内存这块就讲到这里。每种内存都有它的脾气,摸透了,性能优化就成功了一半。我当年刚学CUDA时,就是靠这张内存层次结构图,一步步把性能从"能跑"优化到"飞起"。