第四章:内存层次结构——全局内存、共享内存、寄存器、本地内存、常量内存、纹理内存的物理位置与访问速度对比
说实话,刚接触CUDA那会儿,我最大的困惑就是——显存到底在哪?为什么有的内存快,有的慢?
后来踩了不少坑才明白:GPU的内存不是一块铁板,而是一个层次分明的金字塔。你想想看,CPU有L1/L2/L3缓存,GPU也有类似的设计,只不过名字和用法不太一样。
今天咱们就把这六种内存掰开揉碎了讲清楚。我会结合我实际项目中的经验,告诉你什么时候该用哪种内存,什么时候千万别用。
4.1 全局内存(Global Memory)—— 最常用,也最慢
物理位置: 在显存(DRAM)上,也就是显卡板子上那几颗黑色的显存颗粒。
访问速度: 大约 400-800 GB/s(取决于显存类型,比如GDDR6X)。延迟在 400-800 个时钟周期左右。
说白了,全局内存就是GPU的“硬盘”。所有线程都能读写,容量最大(几GB到几十GB),但速度最慢。
我个人的习惯: 全局内存只用来存输入数据和最终结果。中间计算过程,尽量别碰它。
为什么慢?因为数据要从显存搬到计算单元,中间隔着好几层总线。你想想看,一个线程要读一个float,得等几百个周期,这期间计算单元只能干等着。
避坑指南: 我曾经在项目里把中间变量全放全局内存,结果性能惨不忍睹。后来改成共享内存,速度直接翻了3倍。
4.2 共享内存(Shared Memory)—— 线程块的“私房钱”
物理位置: 在SM(流多处理器)内部,是SRAM(静态随机存取存储器),不是显存。
访问速度: 大约 10-30 个时钟周期。比全局内存快 20-50 倍。
共享内存是每个线程块私有的。同一个块里的线程可以互相读写。这玩意儿是CUDA优化的核心武器。
我记得有一次做矩阵乘法,数据量不大,但反复读全局内存导致带宽瓶颈。我把子矩阵搬到共享内存里,每个线程只从共享内存读数据,性能提升非常明显。
注意: 共享内存容量很小,一般只有 48KB 到 164KB 每个SM。别想着把整个数组塞进去。我见过有人试图把 1MB 的数据放共享内存,结果编译直接报错。
4.3 寄存器(Register)—— 最快,但最稀缺
物理位置: 在SM内部的寄存器文件中,是SRAM。
访问速度: 0 延迟(一个时钟周期内完成读写)。
寄存器是每个线程私有的。你声明的局部变量,只要编译器觉得够小,就会放在寄存器里。
为什么说它快?因为寄存器就在计算单元旁边,数据随取随用。没有总线延迟,没有缓存命中问题。
但有个坑: 每个线程能用的寄存器数量有限。比如一个SM有65536个寄存器,你开了1024个线程,那每个线程最多分到64个。如果你用了80个,编译器就会把多余的“溢出”到本地内存——速度瞬间掉下来。
我的经验: 写kernel时,尽量少声明大数组。能用int就别用float数组。我曾经为了省寄存器,把一个float[4]拆成四个单独的float变量,性能提升了15%。
4.4 本地内存(Local Memory)—— 寄存器的“备胎”
物理位置: 也在显存上,和全局内存是同一块物理区域。
访问速度: 和全局内存一样慢(400-800周期)。
本地内存听起来像“本地”的,其实它就是个幌子。当寄存器不够用时,编译器会把变量放到本地内存里。每个线程有自己的本地内存空间,但物理上它还是显存。
为什么会这样?因为寄存器数量有限。你声明的数组太大,或者递归调用太深,寄存器放不下,就只能“溢出”到本地内存。
我曾经踩过的坑: 写了一个递归的归并排序kernel,结果每个线程用了上百个寄存器,全溢出到本地内存。性能比CPU还慢。后来改成迭代版本,寄存器使用量降下来,速度才正常。
4.5 常量内存(Constant Memory)—— 只读,但带缓存
物理位置: 也在显存上,但有一块专用的缓存(Constant Cache)在SM内部。
访问速度: 如果缓存命中,和寄存器一样快(1周期)。如果缓存未命中,和全局内存一样慢。
常量内存适合存那些所有线程都要读的、不会变的数据。比如卷积核的权重、查找表、数学常数。
为什么它快?因为常量内存有专门的缓存。当所有线程读同一个地址时,缓存命中率极高。但如果你每个线程读不同的地址,缓存就废了。
我建议: 把那些只读的、所有线程共享的查找表放在常量内存里。比如做图像处理时,把伽马校正表放常量内存,效果很好。
4.6 纹理内存(Texture Memory)—— 为空间局部性而生
物理位置: 也在显存上,但有专门的纹理缓存(Texture Cache)在SM内部。
访问速度: 和常量内存类似,缓存命中时很快,未命中时慢。
纹理内存最初是为图形渲染设计的。它擅长处理二维或三维的空间数据,比如图像、体数据。它有硬件支持的插值、边界处理等功能。
为什么用纹理内存?因为它的缓存对空间局部性做了优化。如果你访问一个像素,它周围的像素也会被预取到缓存里。这对图像处理非常友好。
我的经验: 做图像卷积时,用纹理内存比全局内存快20-30%。因为纹理缓存会自动预取相邻像素,减少了显存访问次数。
4.7 速度对比一览表
| 内存类型 | 物理位置 | 访问延迟(周期) | 容量 | 作用域 |
|---|---|---|---|---|
| 寄存器 | SM内部(SRAM) | 0-1 | 几十KB/SM | 单个线程 |
| 共享内存 | SM内部(SRAM) | 10-30 | 48-164KB/SM | 线程块内 |
| 常量内存 | 显存 + 常量缓存 | 1(缓存命中)/ 400+(未命中) | 64KB | 所有线程(只读) |
| 纹理内存 | 显存 + 纹理缓存 | 1(缓存命中)/ 400+(未命中) | 取决于显存 | 所有线程(只读) |
| 本地内存 | 显存(DRAM) | 400-800 | 取决于显存 | 单个线程 |
| 全局内存 | 显存(DRAM) | 400-800 | 几GB-几十GB | 所有线程 |
4.8 知识体系结构图
下面这张图展示了GPU内存层次结构的全貌。你可以看到数据从显存到计算单元要经过哪些层级。
4.9 实战建议:怎么选?
好了,理论讲完了。咱们来点实际的。你在写kernel时,该怎么选内存?
- 能用寄存器,就别用别的。 局部变量尽量少,数组尽量小。
- 需要线程间通信,用共享内存。 比如归约、扫描、矩阵分块。
- 只读的全局数据,用常量内存。 前提是所有线程读同一个地址。
- 二维/三维空间访问,用纹理内存。 图像处理、体数据渲染首选。
- 实在没办法,才用全局内存。 而且要注意合并访问(coalesced access)。
最后提醒一句: 别为了优化而优化。先用全局内存写出正确的版本,再分析瓶颈在哪。我见过有人一上来就用共享内存,结果代码复杂得自己都看不懂,性能还没提升多少。
嗯,内存层次结构这块就讲这么多。记住一句话:数据离计算单元越近,速度越快。 这是GPU优化的黄金法则。
想要更多资料,联系可可