4.1 内存模型概述
Abstract: 本文介绍CUDA编程的内存模型概述,主要讲解CUDA包含的几种内存,以及各种内存的主要特点和用途,这篇像地图一样,指导我们后面的写作和学习。
Keywords: CUDA内存模型,CUDA内存层次结构,寄存器,共享内存,本地内存,常量内存,纹理内存,全局内存
内存模型概述
废话少说,我们直接进入主题。如果说我进入编程行业印象最深刻的一本书,看过我博客的人应该能猜到,我也不止一次地向大家推荐过《深入理解计算机系统》,那本书为我介绍 了几乎所有的计算机基础知识和编程基础知识,真的很基础,里面有CPU结构、内存管理模型、汇编等等,从知识层次来讲,非常偏底层,但是难度确实够让人难受。那本书,我估计我只看了一半,看懂的应该有一半的三分之二,也就是我只看懂了全书的三分之一,推荐有时间一定要看看。
内存访问和管理是程序效率的关键点,高性能计算更是如此,上一篇举的例子关于运输原材料的例子,就是我们平时天天遇到的问题。我们希望有大量的高速度的大容量内存可以给我们的工厂(GPU核心)输送数据,但是根据我们目前的技术,大容量高速的内存不仅造价高,而且不容易生产。到目前为止(2018年5月),计算架构还是普遍采用内存层次模型来获得最佳的延迟和带宽。
CUDA也采用了内存层次模型,结合了主机和设备内存系统,展现了完整的内存层次模型,其中大部分内存我们可以通过编程控制,来使我们的程序性能得到优化。
如果你之前写的程序都没怎么管理过内存,那请先练习下C语言,可能会有更好的理解。
内存层次结构的优点
程序具有局部性特点,包括:
- 时间局部性
- 空间局部性
解释一下,时间局部性,就是一个内存位置的数据某时刻被引用,那么在此时刻附近也很有可能被引用,随时间流逝,该数据被引用的可能性逐渐降低。
空间局部性,如果某一内存位置的数据被使用,那么附近的数据也有可能被使用。
现代计算机的 内存结构主要如下:

这个内存模型在程序局部性原则成立的时候有效。学习过串行编程的人也应该知道内存模型,速度最快的是寄存器,它能和CPU同步地配合,接着是缓存,在CPU片上,然后是主存储器,现在常见的就是内存条,显卡上也有内存芯片,然后是硬盘,这些内存设备的速度和容量成反比,越快的越小,越慢的越大。
局部性是个非常有趣的现象,首先局部性的产生并不是因为设备的原因,而是程序从一开始被编写就有这个特征,与生俱来,所以当我们发现此特征后,就开始设计满足此特征的硬件结构,也就是内存层次模型。当内存模型设计成如上结构的时候,如果你想写快速高效的程序,就要让自己的程序局部性足够好,所以这就进入了一个良性循环,最后为了追求高效率,设备将越来越优化局部性,而程序也会越来越局部化。
总结下最底层(硬盘磁带之类的)的特点:
- 每个比特位的价格要更低
- 容量要更高
- 延迟较高
- 处理器访问频率低
CPU和GPU的主存都是采用DRAM——动态随机存取存储器,而低延迟的内存,比如一级缓存,则采用SRAM——静态随机存取存储器。虽然底层的存储器延迟高,容量大,但是其中有数据被频繁使用的时候,就会向更高一级的层次传输,比如我们运行程序处理数据的时候,程序第一步就是把硬盘里的数据传输到主存里面。
GPU和CPU的内存设计有相似的准则和模型。但它们的区别是:CUDA编程模型将内存层次结构更好地呈现给开发者,让我们显式地控制其行为。
CUDA内存模型
对于程序员来说,分类内存的方法有很多种,但是对于我们来说最一般的分法是:
- 可编程内存
- 不可编程内存
对于可编程内存,如字面意思,你可以用你的代码来控制这组内存的行为;相反的,不可编程内存是不对用户开放的,也就是说其行为在出厂后就已经固化了。对于不可编程内存,我们能做的就是了解其原理,尽可能地利用规则来加速程序,但对于通过调整代码提升速度来说,效果很一般。
CPU内存结构中,一级二级缓存都是不可编程(完全不可控制)的存储设备。
另一方面,CUDA内存模型相对于CPU来说那是相当丰富了,GPU上的内存设备有:
- 寄存器
- 共享内存
- 本地内存
- 常量内存
- 纹理内存
- 全局内存
上述各种都有自己的作用域、生命周期和缓存行为。CUDA中每个线程都有自己的私有的本地内存;线程块有自己的共享内存,对线程块内所有线程可见;所有线程都能访问读取常量内存和纹理内存,但是不能写,因为它们是只读的;全局内存、常量内存和纹理内存空间有不同的用途。对于一个应用来说,全局内存、常量内存和纹理内存有相同的生命周期。下图总结了上面这段话,后面的大篇幅文章就是挨个介绍这些内存的性质和使用的。

寄存器
寄存器无论是在CPU还是在GPU都是速度最快的内存空间,但是和CPU不同的是GPU的寄存器储量要多一些,而且当我们在核函数内不加修饰的声明一个变量,此变量就存储在寄存器中,但是CPU运行的程序有些不同,只有当前在计算的变量存储在寄存器中,其余在主存中,使用时传输至寄存器。在核函数中定义的有常数长度的数组也是在寄存器中分配地址的。
寄存器对于每个线程是私有的,寄存器通常保存被频繁使用的私有变量,注意这里的变量一定不能是共有的,不然的话彼此之间不可见,就会导致大家同时改变一个变量而互相不知道。寄存器变量的生命周期和核函数一致,从开始运行到运行结束,执行完毕后,寄存器就不能访问了。
寄存器是SM中的稀缺资源,Fermi架构中每个线程最多63个寄存器。Kepler结构扩展到255个寄存器,一个线程如果使用更少的寄存器,那么就会有更多的常驻线程块,SM上并发的线程块越多,效率越高,性能和使用率也就越高。
那么问题就来了,如果一个线程里面的变量太多,以至于寄存器完全不够呢?这时候寄存器发生溢出,本地内存就会过来帮忙存储多出来的变量,这种情况会对效率产生非常负面的影响,所以,不到万不得已,一定要避免此种情况发生。
为了避免寄存器溢出,可以在核函数的代码中配置额外的信息来辅助编译器优化,比如:
__global__ void
__launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor)
kernel(...) {
/* kernel code */
}
这里面在核函数定义前加了一个关键字launch_bounds,然后它后面对应了两个变量:
- maxThreadsPerBlock:线程块内包含的最大线程数,线程块由核函数来启动
- minBlocksPerMultiprocessor:可选参数,每个SM 中预期的最小的常驻内存块参数。
注意,对于一定的核函数,优化的启动边界会因为不同的架构而不同。
也可以在编译选项中加入:
-maxrregcount=32
来控制一个编译单元里所有核函数使用的最大寄存器数量。
CUDA Core 与 寄存器
中文解释: CUDA core只是执行单元,类似于CPU中的ALU(算术逻辑单元)。它们负责执行浮点和整数运算,但本身不拥有存储空间。把CUDA core想象成工厂里的工人,而寄存器则是共享的工具箱。
English Explanation: CUDA cores are merely execution units, similar to ALUs (Arithmetic Logic Units) in CPUs. They perform floating-point and integer operations but don't own storage space themselves. Think of CUDA cores as workers in a factory, while registers are shared toolboxes.
寄存器的真实组织方式
SM单元├── 寄存器文件 (Register File)