【CUDA编程】学习笔记(四) GPU存储与优化

一、CPU内存

●现代计算机体系结构面临的主要挑战
✓如果数据无法快速移入和移出,那么快速计算将毫无意义
✓需要大量内存用于大型应用程序
✓非常快的内存也非常昂贵
●最终被推向分层设计

(1)CPU内存层次结构

在这里插入图片描述
●执行速度依赖于利用数据局部性
✓时间局部性:刚刚访问的数据项很可能在不久的将来再次使用,因此请将其保留在缓存中
✓空间局部性:相邻数据也可能很快被使用,因此加载 它们使用“宽”总线(如多车道高速公路)同时进入缓存
●宽总线只能获得高带宽以减缓主存

(2)缓存

●高速缓存行是数据传输的基本单位
✓典型大小为64字节≡8×8字节项
●使用单个高速缓存时,CPU将数据加载到寄存器中:
✓它在高速缓存中查找行
✓如果有(它获取数据)
✓如果没有(未命中),它从主存储器获取整行,取代缓存中的现有行(通常最近最少使用)
●当CPU存储来自寄存器的数据时:
✓怎么办?

二、GPU内存

(1)开普勒GPU

在这里插入图片描述
SM有公共的全局内存,每个SM内部有自己的共享内存。

(2)开普勒内存层次结构

在这里插入图片描述

(3)开普勒缓存

●缓存行
✓L1:通常为128字节缓存行(32个浮点数或16个双精度)
✓L2:在某些情况下为32字节
●带宽
✓从设备内存到L2缓存的384位内存总线
✓高达250 GB / s带宽
●容量
✓为所有SMX提供统一的1.5MB二级高速缓存
✓每个SMX具有48kB的共享内存/ L1高速缓存(分为16 / 48,32 / 32或48/16)
●一致性
✓无CPU中的全局高速缓存一致性
在这里插入图片描述
合并转移
在这里插入图片描述
在这里插入图片描述
比较这两段代码前者warp中的32个线程将寻址数组x的相邻元素,使x [0]位于缓存行的开头,则x [0] - x [31]将位于相同的缓存行 - 即“合并”转移,因此获得了完美的空间局部性,而后者则不满足

三、变量

(1)内置向量类型

●Integer和Float结构最多包含四个组件
✓基本类型:char,short,int,long,longlong,float,double
✓派生类型:char1,uchar1,char2,uchar2,int1,uint1,int2,uint2,int3, uint3 …
✓组件可通过字段x,y,z和w来访问
✓构造函数(例如):int2 make_int2(int x,int y);
●dim3
✓基于uint3的整数向量类型
✓未指定的组件初始化为1

(2)内置变量

●gridDim,blockDim
✓类型为dim3
✓包含gird/block的尺寸
●blockIdx,threadIdx
✓类型为uint3
✓包含网格/块内的块索引
●warpSize
✓类型为int
✓包含线程中的warp大小

(3)用户定义变量

●变量类型限定符
✓指定变量设备上的内存位置
●__device__
✓驻留在全局内存空间中
●__shared__
✓驻留在线程块的共享内存空间中
●__constant__
✓驻留在常量内存空间中
●__managed__
✓可以引用 从设备和主机代码
●__restrict__
✓在C99中引入
●寄存器变量

(4)全局数组

目前,主要是采用下面这种模式
在这里插入图片描述
全局变量也可以通过内核代码文件中具有全局范围的声明来创建
在这里插入图片描述
●__device__ prefix告诉nvcc这是GPU中的全局变量,而不是CPU
●变量可以被任何内核读取和修改
●它的生命周期是整个应用程序的生命周期
●也可以声明固定大小的数组
●可以读取 / 写入通过在主机代码中使用特殊例程cudaMemcpyToSymbol,cudaMemcpyFromSymbol或标准cudaMemcpy与cudaGetSymbolAddress结合使用主机代码

(5)恒定变量

●非常类似于全局变量,除了它们不能由内核修改:
✓使用前缀__constant__
✓在主机代码中使用cudaMemcpyToSymbol,cudaMemcpyFromSymbol或cudaMemcpy结合cudaGetSymbolAddress
__ constant__ float constData在内核文件中定义全局范围[256];
cudaMemcpyToSymbol(constData,data,sizeof(data));
cudaMemcpyFromSymbol(data,constData,sizeof(data));
✓__constant__变量只能通过运行时函数从主机代码中分配; 它们不能从设备代码中分配
✓__global__函数参数通过常量内存传递给设备,限制为4 KB
●只有64KB的常量内存,但是好处是每个SMX都有一个恒定的缓存
✓当所有线程读取相同的常量时,几乎与寄存器一样快
✓不会占用寄存器,因此非常有助于最小化总数 需要寄存器
✓(在Fermi GPU上,常量缓存也用于在内核中声明为只读的全局数组,并由所有线程统一访问,即所有线程读取相同的元素)

(6)常量

●常量变量的值在运行时设置
●但代码也经常有普通常量,其值在编译时已知:
#define PI 3.1415926f
a = b /(2.0f * PI);
●保持原样 - 它们似乎嵌入到可执行代码中,因此它们不会耗尽任何寄存器
●如果您想要单精度,请不要忘记最后的f;
在C / C ++中single × double = double

(7)寄存器

在每个内核中,默认情况下,将各个变量分配给寄存器:
在这里插入图片描述
●Kepler:每个SMX有65536个32位寄存器
●每个线程K20 / K20X最多255个
✓费米:每个线程最多63个寄存器
●Kepler:每个SMX最多2048个线程(每个线程块最多1024个)
✓如果最大寄存器 每个线程⇒25个线程用于K20 / K20X(费米:1024个线程)
✓如果使用最大线程⇒每个线程32个寄存器
●如果应用需要更多寄存器会怎样?
✓他们“溢出”到L1缓存或设备内存 - 精确机制不清楚,但
✓某些变量成为device arrays ,每个线程有一个元素
✓或者某些寄存器的内容被“保存”到设备内存,因此可以用于其他目的,然后数据在以后“恢复”
✓无论哪种方式,应用程序都受到使用设备内存的延迟和带宽影响

(8)局部数组

如应用使用了一个小数组会怎样?
在这里插入图片描述
●在这种简单的情况下(很常见)编译器转换为向量寄存器:
在这里插入图片描述
●在更复杂的情况下,它将数组放入设备内存
●文档中仍称为“局部数组”,因为每个线程都有自己的私有副本
●默认情况下保存在L1缓存中,可能永远不会传输到设备内存
● 16kB的L1缓存相当于4096个32位变量,当使用1024个线程时,每个线程只有4个
●超出此范围,它将不得不溢出到设备内存

(8)共享内存

●在内核中,前缀__shared__
例如:__ shared__ int x_dim__shared__;
__ shared__ float x [128];
✓声明共享变量在线程块中的所有线程之间共享
✓驻留在共享内存中
✓块的任何线程都可以设置其值,或者读取它
●有几个好处:
✓对于需要线程之间通信的操作至关重要
✓对数据重用有用
✓当变量对所有线程具有相同值时,替代设备存储器中的局部数组可减少寄存器的使用
在这里插入图片描述
●静态分配的共享内存
✓大小在编译时已知
●动态共享内存数组
✓启动内核时,可选的第三个参数指定总大小:KernelFunc <<< gridDim,blockDim,nSMem,iStream >>>(参数);
●Kepler有64KB,在L1缓存和共享内存之间分为16 / 48,32 / 32或48/16。这个分割可以由程序员使用
cudaFuncSetCacheConfig或者cudaDeviceSetCacheConfig
✓如果未通过cudaDeviceSetCacheConfig设置,则默认为48KB的共享内存
✓如果内核不需要太多共享内存,则可能切换到16KB的共享内存

(9)纹理内存

●最初主要用于纯图形应用程序
在这里插入图片描述
●纹理和表面存储空间驻留在设备存储器中并缓存在纹理缓存中
✓纹理缓存针对2D空间局部性进行了优化
✓对于不能方便地遵循合并约束的应用,纹理映射硬件提供了令人满意的替代方案
●通过CUDA数组进行纹理处理
✓ texture<unsigned char, 2> texTemplate;
✓ cudaArray *pArrayTemplate = NULL;
✓ cudaChannelFormatDesc desc = cudaCreateChannelDesc();
✓ cudaMallocArray(&pArrayTemplate, &desc, w, h);
✓cudaMemcpy2DArrayToArray(pArrayTemplate, 0, 0, hTemplate, 0, 0, w, h, cudaMemcpyHostToDevice);
✓ cudaBindTextureToArray(texTemplate, pArrayTemplate);
✓ unsigned char T =tex2D(texTemplate,(float)xTemplate + x,(float)yTemplate + y);
●全局内存纹理(自SM3.x起)
✓硬件增加了通过纹理缓存层次结构读取全局内存的能力,无需设置和绑定纹理引用
✓需要使用限定符声明全局数组
const __ restrict__
✓或使用内在函数__ldg()

(10)变量

在这里插入图片描述

(11)每个SM中激活的blocks

●每个块需要一定的资源:
✓线程
✓寄存器(每个线程的寄存器数×线程数)
✓共享存储器(静态+动态)
●它们共同决定每个SMX上可以同时运行多少个块
✓最多16个块

四、内存优化

(1)三个瓶颈

●内存带宽限制:
✓GDDR/ L2 / TEX / L1 /共享内存带宽等。
●指令吞吐量限制:
✓单/双精度/ LDST / SFU吞吐量等。
●延迟限制:
✓等待时没有足够的线程束切换。

(2)优化原则

●最小化带宽的数据传输
✓最小化CPU-GPU数据传输
•16 GB / s(PCIe x16 Gen3)与250 GB / s(K20X)
✓最大限度地减少全局存储器与设备之间的数据传输(Kepler)
•合并访问
•最大化使用片上存储器(尤其是共享存储器)

(3)CPU-GPU内存转换

●三种策略
✓将更多代码从主机移动到设备
✓将许多小型传输批处理为单个大型传输
✓重叠内存转移与计算
cudaStreamCreate(&stream1); cudaStreamCreate(&stream2); cudaMemcpyAsync(a_d, a_h, size, cudaMemcpyHostToDevice, stream1); kernel<<<grid, block, 0, stream2>>>(otherData_d);

(4)全局内存操作:合并

●每个warp执行内存操作
✓warp中的32个线程提供内存地址
✓硬件确定这些地址落入哪些行
•合并访问:内存事务粒度为32个字节
•对于访问128个连续对齐区域的warp有好处或者 256字节
●访问字大小
✓本机支持的大小(每个线程):1,2,4,8,16字节
•假设每个线程的地址在字大小边界上对齐
✓如果要访问非原生大小的数据类型,编译器将生成多个具有原生大小的加载或存储指令
……

(5)全局内存操作:进入共享内存

……

发布了75 篇原创文章 · 获赞 28 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Swocky/article/details/91049054