基于C++与CUDA的N卡GPU并行程序——内存操作

常量内存

  有时,计算速度的性能瓶颈不在于数学计算速度,而是内存带宽.硬件提供了64kb或者更多的常量内存,用于保存在核函数执行期间不会发生变化的数据,在有些情况中使用常量内存来替换全局内存,能有效减少内存带宽.如果是全局内存,其声明方式很简单,如下所示

Sphere *s;//Sphere是某个自己定义的类,s是声明的类对象指针变量

如果是常量内存,声明方法与共享内存是类似的,常量内存是在前面加上一个修饰符,如下所示

__constant__ Sphere s[SPHERE];

这里需要注意的是,在最初是把s声明为指针变量,然后通过cudaMalloc()来为指针分配内存空间,但是改为常量内存之后,就是静态分配了,直接写明其分配的内存长度s[SPHERE],这与C语言在堆内存和栈内存上分配空间的区别类似,因此,最后也不需要用cudaFree来释放空间.从CPU复制到GPU的过程也要做相应修改,如果是全局内存则为

cudaMemcpy(...,...,size,cudaMemcpyHostToDevice);//复制到全局内存

修改为常量内存之后则为

cudaMemcpyToSymbol(...,...,size);//复制到常量内存

__constant__将对变量的访问限制为只读,这样可以实现节约带宽,对常量内存的单次读取操作可以广播到"邻近"线程,这将节约15次读取操作,因为16个线程是一个半线程束,这时所有的读取操作可以作为一个整体一次性完成.当所有16个线程都读取相同地址时,这样可以提升性能,但当16个线程都分别读取不同的地址时,会降低性能,因为16次不同的读取操作,在常量内存中会串行化,在全局内存中则是并行的.

计时

  为了准确衡量GPU的计算性能,需要对GPU的操作进行计时,其代码如下所示

cudaEvent_t start, stop;                               //声明计时事件
float elapsedTime;                                     //声明时间差变量
cudaEventCreate(&start);                               //创建开始事件
cudaEventCreate(&stop);                                //创建结束事件
cudaEventRecord(start, 0);                             //记录时间戳,其中的参数0表示GPU的第0个流

//在GPU上执行一些操作......

cudaEventRecord(stop, 0);                              //记录GPU时间戳
cudaEventSynchronize(stop);                            //等待同步
cudaEventElapsedTime(&elapsedTime, start, stop);       //计算时间
printf("Time to generate: %3.1f ms\n", elapsedTime);   //打印时间
cudaEventDestroy(start);                               //销毁开始事件
cudaEventDestroy(stop);                                //销毁结束事件

纹理内存

  纹理内存是另一种类型的只读内存,在特定的访问模式中,纹理内存同样能够提升性能并减少内存流量.与常量内存类似的是,纹理内存同样缓存在芯片上,因此在某些情况中,它能够减少对内存的请求并提供更高效的内存带宽.纹理内存是专门为那些在内存访问模式中存在大量空间局部性的图形应用程序而设计的,一个线程读取的位置可能与邻近线程读取的位置"非常接近".

texture<float> texConstSrc;                            //声明纹理内存
cudaMalloc((void**)&dev_constSrc,imageSize);           //分配全局内存
//将全局内存绑定到声明的纹理内存上
cudaBindTexture(NULL, texConstSrc, dev_constSrc, imageSize);
//不使用方括号[]进行下标索引,而是使用texlDfetch(texConstSrc,index)进行下标索引
cudaUnbingTexture(texConstSrc);                        //解除纹理内存的绑定
cudaFree(dev_constSrc);                                //释放全局内存

在某些二维问题中使用二维的线程块和线程格是有用的.如果没有具体说明,默认的纹理内存都是一维的,这时增加一个代表维数的参数,就能声明二维纹理内存.

texture<float, 2> texConstSrc;//声明二维纹理内存

在对纹理内存进行下标索引的调用时,将texlDfetch()修改为tex2D(),在tex2D()的调用中,不再担心发生边界溢出问题,横纵坐标索引值如果小于0,那么tex2D()将返回0处的值,如果大于宽度那么将返回宽度处的值.

t=tex2D(texConstSrc, x, y);//调用二维纹理内存

在随后的纹理内存的绑定过程中也要做相应修改,释放内存的过程则不变,具体如下所示

cudaChannelFormatDesc desc = cudaCreateChannelDesc<float>();//设置通道描述
//纹理内存的绑定
cudaBindTexture2D(NULL, texConstSrc, dev_constSrc, desc, DIM, DIM, sizeof(float)*DIM);
cudaUnbingTexture(texConstSrc);                        //解除纹理内存的绑定
cudaFree(dev_constSrc);                                //释放全局内存

如果多个线程操作同一个变量,需要使用原子操作,如果线程过多,将会发生竞争使其变成串行,这时可以先在共享内存上执行原子求和,再在全局内存上执行原子求和.

atomicAdd(&(histo[buffer[i]]), 1);//原子操作加1

页锁定内存

  malloc()是在CPU端开辟的可分页内存,是操作系统对物理内存进行虚拟化之后的,所以如果内存资源紧张,操作系统会悄悄的将虚拟内存的数据转移到磁盘空间上,等到需要用的时候又复制回来.在GPU端访问主机的可分页内存时,会首先访问一部分数据以确认其内存地址没有改变,然后再重新复制所有数据,所以这里的效率并不高.
  为此可以使用cudaHostAlloc()开辟主机端的页锁定内存,也称不可分页内存或固定内存.相比于可分页内存,访问复制的速度更快,操作系统不会对这块内存进行分页并转移到磁盘上,可以使应用程序专门访问这块物理内存,但是缺点是会随之丧失虚拟内存的特性,不过为了提升性能可以忽略这一点.同时,如果系统的内存比较紧张,那么使用页锁定内存将会使内存资源更快耗尽,所以使用完之后立即释放内存资源,而不是等应用程序结束之后才释放.主机端页锁定内存的定义如下所示

int *a;                                                //声明变量
//分配页锁定内存
cudaHostAlloc((void**)&a, size*sizeof(*a), cudaHostAllocDefault);
cudaFreeHost(a);                                       //释放页锁定内存

上述代码出现参数cudaHostAllocDefault是指默认的页锁定内存,还可以定义另一种页锁定内存,称为零拷贝内存

cudaHostAlloc((void**)&a, size*sizeof(*a), cudaHostAllocWriteCombined | cudaHostAllocMapped);

CUDA流

  CUDA流表示一个 GPU操作队列,并且该队列中的操作将以指定的顺序执行,如下所示是创建执行单个CUDA流

cudaStream_t stream;                                   //初始化流
cudaStreamCreate(&stream);                             //创建CUDA流
//异步复制数据
cudaMemcpyAsync(dev_a, host_a+i, N*sizeof(int), cudaMemcpyHostToDevice, stream);
kernel<<<N/256, 256, 0,stream>>>(dev_a, dev_b, dev_c);
cudaStreamSynchronize(stream);                         //对指定的流,进行同步
cudaStreamDestroy(stream);                             //销毁CUDA流

如果创建两个CUDA流,A流在执行计算时,B流在传输计算数据,然后当A流传输计算数据时,B流正好在执行计算,这样就可以提升效率.但是需要注意的是,如果同时调度某个CUDA流的所有操作,那么很容易在无意中阻塞另一个CUDA流的复制操作或者核函数计算操作.要解决这个问题,在将操作放入CUDA流的队列时应采用宽度优先的方式.不是首先添加第A个流的所有操作,然后添加B流的所有操作,而是将这两个CUDA流之间的操作交叉添加.假设A和B两个流同时计算a和b两组数相加,最后产生c,每个流都有四个操作分别是,1.复制a到GPU;2.复制b到GPU;3.计算c=a+b;4.复制c回CPU,那么两个CUDA流的添加顺序是

A流    复制a
B流    复制a
A流    复制b
B流    复制b                A流计算求和
A流    复制c                B流计算求和
B流    复制c

资料简介

  CUDA工具箱还提供了两个常用的工具库,称为快速傅里叶变换库CUFFT和线性代数函数库CUBLAS,同时也有公司提供了一个功能更加丰富的线性代数函数库CULAtools,可以在以下网站下载免费基本版本.BLAS最初是一个由FORTRAN编写的线性代数函数库,数组采用列主形式的存储布局,而不是C,C++的行主布局.

www.culatools.com/versions/basic

还有一个性能原语NPP,主要侧重于处理图像和视频,免费下载地址为

www.nvidia.com/object/npp.html

CUDA的代码调试工具有CUDA-GDB,CUDA Visual Profiler可以进行代码性能分析
伊利诺斯大学有CUDA C编程课程,同时官方网站上也有在线课程

www.nvidia.com/object/cuda_education

CUDA相关的论坛有

http://forums.nvidia.com
https://developer.nvidia-china.com

可以参考书籍Programming Massively Parallel Processors:a Hands-On Approachs

猜你喜欢

转载自blog.csdn.net/wanchaochaochao/article/details/105837000
今日推荐