[CUDA编程] --- CUDA内存模型

1 内存层次结构的优点

一般来说,应用程序不会在某一时间点访问任意数据或运行任意代码。应用程序往往遵循局部性原则,这表明它们可以在任意时间点访问相对较小的局部地址空间。有两种不同类型的局部性:

时间局部性:时间局部性认为如果一个数据位置被引用,那么该数据在较短的时间周期内很可能会再次被引用,随着时间流逝,该数据被引用的可能性逐渐降低。
空间局部性:空间局部性认为如果一个内存位置被引用,则附近的位置也可能会被引用。
通常,随着从处理器到内存延迟的增加,内存的容量也在增加。一个典型的层次结构如图所示。
在这里插入图片描述

CPU和GPU的主存都采用的是DRAM(动态随机存取存储器),而低延迟内存(如CPU一级缓存)使用的则是SRAM(静态随机存取存储器)。

GPU与CPU在内存层次结构设计中都使用相似的准则和模型。GPU和CPU内存模型的主要区别是,CUDA编程模型能将内存层次结构更好地呈现给用户,能让我们显式地控制它的行为。也就是我们可以使用程序更好的控制GPU的内存。

2 CUDA内存模型

在这里插入图片描述

上图为CUDA内存模型,包括:

  • 寄存器(Registers)
  • 共享内存(Shared Memory)
  • 局部内存(Local Memory)
  • 常量内存(Constant Memory)
  • 纹理内存(Texture memory)
  • 全局内存 Global Memory

他们拥有不同的作用范围,生命周期以及缓存机制。简言之,

  • 个线程拥有自己私有的寄存器和局部内存(local memory)。
  • 一个block内的所有线程可以读写该block专有的共享内存(shared memory)。
  • 所有线程可以读写全局内存(Global memory),只读常量内存(constant memory)和纹理内存(texture memory)。

2.1 寄存器(Registers)

Registers拥有最快的访问速度。在Kernel函数里定义的变量以及确定长度的数组都可存放在寄存器中。

__global__ void kernelfunc()
{
    float variable;  // in registers
    ...

寄存器属于线程私有资源,SM为每个active block里的线程都分配了寄存器。不同GPU每个线程的最大寄存器数量不同(Fermi为63, Kepler扩展到255)。上一篇文章提到了SM资源限制对active block数量的影响,线程使用的寄存器越少,SM上active block就越多,使SM硬件资源得到更有效的利用。

可以通过如下编译选项查看线程的寄存器,共享内存的使用情况

-Xptxas -v,-abi=no

如果使用的寄存器超出了硬件限制,那么多出来的部分就会存放在Local memory里。CUDA编译器提供了关键字__launch_bound__启发式地限制寄存器的使用,从而提高SM内active block数量。

__global__ void __launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor) kernel(...) {
 // your kernel body
}

heuristics(启发式)一词,多少让人有点摸不到头脑。 说通俗一点,就是我们在代码中直接告诉编译器,该Kernel函数每个Block分配的最大线程数以及SM最小支持的active block数,这样编译器就可以调整每个线程拥有的寄存器数,优化SM中active block的数量了。

maxThreadPerblock: 参数指定了一个Block内最大线程数。通常情况下,只有在Kernel执行时,才能真正知道Block内的线程数,编译状态下是没法获得的。但是使用__launch_bounds__就可以直接告诉编译器,使用该Kernel函数时,一个Block包含的线程数量,从而让编译器做寄存器分配上的优化。minBlockkPerMultiprocessor: 一个SM最少支持的active block数。这个参数不是必需的,编译器会根据不同的GPU架构赋值。
另一种方法限制线程使用的寄存器的数量是采用如下编译参数,

-maxrregcount=32

这种方式看上去比较粗暴,所有CUDA线程的寄存器都被限制在32个,除非某个Kernel函数显示的使用__launch_bounds__。

2.2 Local Memory

线程私有。当SM内寄存器使用达到上限,或者无法确定数组大小的时候,数据会存放在局部内存。

float variable[100]//In Local Memory

物理上并不存在单独的Local memory。编译器会将其放入片外的DRAM中(与Global Memory相同),访存延迟大,带宽小。

2.3 Shared Memory

共享内存是SM的私有资源,属于on-chip内存,带宽相对较高,延迟也较低。SM将共享内存资源划分给每个active block。上一篇提到了共享资源限制与SM active block之间的关系,使用共享内存越少,active block数量越多。

__shared__ float variable;

Block内的线程共享Shared Memory。可以使用__syncthreads()执行同步操作。该函数会阻塞线程,直到block内所有线程都执行到此函数。

2.4 Constant Memory

常量内存同样是offchip内存,只读,拥有SM私有的constant cache,因此在cache hit的情况下速度快。常量内存是全局的,对所有Kernel函数可见。因此声明要在Kernel函数外,

__constant__ float variable;

2.5 Texture Memory

与常量内存类似,纹理内存也是offchip内存,拥有SM私有的cache,在cache hit的情况下访存速度快,对所有线程可见。纹理内存对2D空间存储做了优化,因此如果Warp访问2D/3D数据,会得到更好的性能,但是对于其他线程,性能反而会差。

texture<type,
dim>
tex_var;        //Initialize
cudaChannelFormatDesc();         //Options
cudaBindTexture2D(...);          //Bind
tex2D(tex_var,
x_index,
y_index);  //Fetch

2.6 Global Memory

offchip内存,所有线程可见。使用cudaMalloc分配的内存就属于Global Memory。另外还可以静态分配,

__device__ float devData;

值得注意的是,静态声明的devData只是一个符号(Symbol),不能像其他Host内存变量那样使用&devData获取其Device内存指针,因此无法使用cudaMemcpy(&devData…)对其赋值。

3 CUDA变量声明总结

用表格进行总结:
在这里插入图片描述

设备存储器的重要特征
在这里插入图片描述
下图表达的更加直观一些。

Local, Global, Contant, Texture为片外DRAM,其中Global, Constant, texture内存在Host端代码声明,所有线程可见。
SM拥有私有的Registers和Shared Memory(其实还有SM私有的L1 cache以及共有的L2 cache,下一篇会详细说明),Constant和Texture内存有专有的Caches, 这些在片上。
在这里插入图片描述
通常来讲,CPU/GPU访存性能瓶颈在访问片外DRAM上,即访问Global, Constant, Local, Texture内存。下一篇会以Global Memory为例,写如何高效的访问DRAM。

参看连接:
https://zmurder.github.io/CUDA/CUDA%20C%E7%BC%96%E7%A8%8B%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97%E7%AC%94%E8%AE%B0/4-2%20%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Overboom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值