CUDA学习日志:常量内存和纹理内存

接触CUDA的时间并不长,最开始是在cuda-convnet的代码中接触CUDA代码,当时确实看的比较痛苦。最近得空,在图书馆借了本《GPU高性能编程 CUDA实战》来看看,同时也整理一些博客来加强学习效果。

Jeremy Lin

在上一篇博文中,我们谈到了如何利用共享内存来实现线程协作的问题。本篇博文我们主要来谈谈如何利用常量内存和纹理内存来提高程序性能。


常量内存

所谓的常量内存,从它的名字我们就可以知道,它是用来保存在核函数执行期间不会发生变化的数据。NVIDIA硬件提供了64KB的常量内存,并且常量内存采用了不同于标准全局内存的处理方式。在某些情况下,用常量内存来替换全局内存可以有效地减少内存带宽。

常量内存的声明方式与共享内存是类似的。要使用常量内存,则需要在变量前面加上 __constant__修饰符:

__constant__ int s[10000]

在之前的程序中,我们为变量分配内存时是先声明一个指针,然后通过cudaMalloc()来为指针分配GPU内存。而当我们将其改为常量内存时,则要将这个声明修改为在常量内存中静态地分配空间。我们不再需要对变量指针调用cudaMalloc()或者cudaFree(),而是在编译时为这个变量(如数组s)提交固定的大小。

另外一个值得注意的是,当从主机内存复制到GPU上的常量内存时,我们需要使用一个特殊版本的cudaMemcpy(),即是:

 cudaMemcpyToSymbol()

CUDA学习日志:常量内存和纹理内存_第1张图片

cudaMemcpyToSymbol()和参数为cudaMemcpyHostToDevice()的cudaMemcpy()之间的唯一差异在于,cudaMemcpyToSymbol()会复制到常量内存,而cudaMemcpy()会复制到全局内存。


note:变量修饰符 __constant__ 将变量的访问限制为只读。

那么常量内存为什么能带来性能提升~

原因:

  • 对常量内存的单次读操作可以广播到其他的“邻近(nearby)”线程,这将节约15次读取操作;
  • 常量内存的数据将缓存起来,因此对于相同地址的连续操作将不会产生额外的内存通信量。

下面我们来具体讲讲这两个原因。

首先,我们需要来看看到底什么是线程束(warp),在CUDA架构中,线程束是指一个包含32个线程的集合,这个线程集合被“编织在一起”并且以“步调一致(Lockstep)”的形式执行。在程序中的每一行,线程束中的每个线程都将在不同的数据上执行相同的指令。

当处理常量内存时,NVIDIA硬件将把单次内存读取操作广播到每个半线程束(Half-Warp)。在半线程束中包含16个线程,即线程束中线程数量的一半。如果在半线程束中的每个线程从常量内存的相同地址上读取数据,那么GPU只会产生一次读取请求并在随后将数据广播到每个线程。如果从常量内存中读取大量数据,那么这种方式产生的内存流量只是使用全局内存时的1/16。

但在读取常量内存时,所节约的并不仅限于减少94%的带宽。由于这块内存的内容是不发生变化的,因此硬件将主动把这个常量数据缓存在GPU上。在第一次从常量内存的某个地址上读取后,当其他半线程束请求同一个地址时,那么将命中缓存,这同样减少了额外的内存流量。

不过正如上一篇博文讲的__syncthread()不能乱用一样,常量内存也不能乱用。它可能会对性能产生负面的影响。半线程束广播功能实际上是一把双刃剑。虽然当所有16个线程都读取相同地址时,这个功能可以极大提升性能,但当所有16个线程分别读取不同的地址时,它实际上会降低性能。因为这16次不同的读取操作会被串行化,从而需要16倍的时间来发出请求。但如果从全局内存中读取,那么这些请求会同时发出。


纹理内存

和常量内存一样,纹理内存是另一种类型的只读内存,在特定的访问模式中,纹理内存同样能够提升性能并减少内存流量。

纹理内存缓存在芯片上,因此在某些情况中,它能够减少对内存的请求并提供更高效的内存带宽。纹理缓存是专门为那些在内存访问模式中存在大量空间局部性(Spatial Locality)的图形应用程序而设计的。在某个计算应用程序中,这意味着一个线程读取的位置可能与邻近线程的读取位置“非常接近”,如下图所示。


从数学的角度,上图中的4个地址并非连续的,在一般的CPU缓存中,这些地址将不会缓存。但由于GPU纹理缓存是专门为了加速这种访问模式而设计的,因此如果在这种情况中使用纹理内存而不是全局内存,那么将会获得性能的提升。

下面,我们来看看如何使用纹理内存。

首先,需要将输入的数据声明为texture类型的引用。比如:

texture<float>  texConst;


然后,就是在为这三个缓冲区分配GPU内存后,需要通过cudaBindTexture()将这些变量绑定到内存缓存区。这相当于告诉CUDA运行时两件事:

  • 我们希望将指定的缓冲区作为纹理来使用;
  • 我们希望将纹理引用作为纹理的“名字”。



当用cudaBindTexture绑定后,纹理变量就设置好了,现在可以启动核函数。

然而,当读取核函数中的纹理时,需要通过特殊的函数来告诉GPU将读取请求转发到纹理内存而不是标准的全局内存。因此,当读取内存时,需要使用特殊的方式:
从线性内存中读取(拾取),使用的函数是tex1Dfetch()

template<class Type> 
Type tex1Dfetch( 
 texture<Type, 1, cudaReadModeElementType> texRef, 
 int x); 
float tex1Dfetch( 
 texture<unsigned char, 1, cudaReadModeNormalizedFloat> texRef, 
 int x); 
float tex1Dfetch( 
 texture<signed char, 1, cudaReadModeNormalizedFloat> texRef, 
 int x); 
float tex1Dfetch( 
 texture<unsigned short, 1, cudaReadModeNormalizedFloat> texRef, 
 int x); 
float tex1Dfetch( 
 texture<signed short, 1, cudaReadModeNormalizedFloat> texRef, 
 int x);

这些函数用纹理坐标x拾取绑定到纹理参考texRef 的线性内存的区域。不支持纹理过滤和寻址模式。对于整数型,这些函数将会将整数型转化为单精度浮点型。除了这些函数,还支持2-和4-分量向量的拾取。

最后,当应用程序运行结束后,还要清除纹理的绑定。

cudaUnbindTexture(texConst);

CUDA学习日志:常量内存和纹理内存_第2张图片


上面谈的都是一维纹理内存,实际上也可以使用二维纹理内存

二维纹理内存的声明如下:

texture<float,2> texConst;

绑定函数:

cudaBindTexture2D()

CUDA学习日志:常量内存和纹理内存_第3张图片


纹理拾取函数:

tex2D()

CUDA学习日志:常量内存和纹理内存_第4张图片


本文地址:http://blog.csdn.net/linj_m/article/details/41522573

更多资源请 关注博客:LinJM-机器视觉  微博:林建民-机器视觉


你可能感兴趣的:(CUDA,LinJM,常量内存,纹理内存)