1、常量内存
NVIDIA GPU卡从逻辑上对用户提供了64KB的常量内存空间,可以用来存储内核执行期间所需要的恒定数据。常量内存对一些特定情况下的小数据量的访问具有相比全局内存的额外优势。使用常量内存也一定程度上减少了对全局内存的带宽占用。
话不多说,直接coding吧:
#include "stdio.h" #include<iostream> #include <cuda.h> #include <cuda_runtime.h> //使用__constant__定义两个常量constant_f,constant_g __constant__ int constant_f; __constant__ int constant_g; #define N5 //定义设备的Kernel函数 __global__ void gpu_constant_memory(float *d_in, float *d_out) { //线程的index int tid = threadIdx.x; d_out[tid] = constant_f * d_in[tid] + constant_g; } int main(void) { //定义主机的数组 float h_in[N], h_out[N]; //定义设备指针 float *d_in, *d_out; int h_f = 2; int h_g = 20; // 申请cpu内存 cudaMalloc((void**)&d_in, N * sizeof(float)); cudaMalloc((void**)&d_out, N * sizeof(float)); //初始化数组 for (int i = 0; i < N; i++) { h_in[i] = i; } //把主机上的数组复制到设备上 cudaMemcpy(d_in, h_in, N * sizeof(float), cudaMemcpyHostToDevice); //把定义的常量复制到GPU设备的常量内存中 cudaMemcpyToSymbol(constant_f, &h_f, sizeof(int), 0, cudaMemcpyHostToDevice); cudaMemcpyToSymbol(constant_g, &h_g, sizeof(int)); //使用1个N个线程的Block调用和执行Kernel函数 gpu_constant_memory << <1, N >> > (d_in, d_out); //把设备运算的结果传回CPU主机 cudaMemcpy(h_out, d_out, N * sizeof(float), cudaMemcpyDeviceToHost); //打印结果 printf("Use of Constant memory on GPU \n"); for (int i = 0; i < N; i++) { printf("The expression for input %f is %f\n", h_in[i], h_out[i]); } //释放GPU设备内存 cudaFree(d_in); cudaFree(d_out); return 0; }
常量内存中的变量使用__constant__关键字修饰。在之前的代码中,两个浮点数constant_f,constant_g被定义成在内核执行期间不会改变的常量。同时使用__constant__(在内核外面)定义好了它们后,它们不应该再次在内核内部定义。
在main函数中,h_f,h_g两个常量在主机上被定义并初始化,然后将被复制到设备上的常量内存中。
使用cudaMemcpyToSymbol函数把这些常量复制到内核执行所需要的常量内存中。
该函数具有五个参数:
第一个参数:是(要写入的)目标,也就是我们刚才用__constant__定义过的h_f或者h_g常量;
第二个参数:是源主机地址;
第三个参数:是传输大小;
第四个参数:是写入目标的偏移量,这里是0;
第五个参数:是设备到主机的数据传输方向;
最后两个参数是可选的,因此后面第二次cudaMemcpyToSymbol函数调用的时候省略掉了它们。
2、纹理内存
纹理内存是另外一种当数据的访问具有特定的模式的时候能够加速程序执行,并减少显存带宽的只读存储器。像常量内存一样,它也在芯片内部被cache缓冲。该存储器最初是为了图形绘制而设计的,但也可以被用于通用计算。当程序进行具有很大程度上的空间邻近性的访存的时候,这种存储器变得非常高效。
空间邻近性的意思是,每个线程的读取位置都和其他线程的读取位置邻近。这对那些需要处理4个邻近的相关点或者8个邻近的点的图像处理应用非常有用。一种线程进行2D的平面空间邻近性的访存的例子如下表:
通用的全局内存的cache将不能有效处理这种空间邻近性,可能会导致进行大量的显存读取传输。纹理存储被设计成能够利用这种访存模型,这样它只会从显存读取1次,然后缓冲掉,所以执行速度将会快得多。纹理内存支持2D和3D的纹理读取操作,在你的CUDA程序里面使用纹理内存可没有那么轻易,特别是对那些并非编程专家的人来说。
#include "stdio.h" #include<iostream> #include <cuda.h> #include <cuda_runtime.h> #define NUM_THREADS 10 #define N 10 //通过【纹理引用】来定义一段能进行纹理拾取的纹理内存 texture <float, 1, cudaReadModeElementType> textureRef; __global__ void gpu_texture_memory(int n, float *d_out) { int idx = blockIdx.x*blockDim.x + threadIdx.x; if (idx < n) { float temp = tex1D(textureRef, float(idx)); d_out[idx] = temp; } } int main() { //计算Block的个数,用于后面的使用 int num_blocks = N / NUM_THREADS + ((N % NUM_THREADS) ? 1 : 0); //声明设备的指针 float *d_out; // 在设备上为结果申请显存 cudaMalloc((void**)&d_out, sizeof(float) * N); // 在主机设备上为结果申请内存 float *h_out = (float*)malloc(sizeof(float)*N); //声明并初始化主机的数组 float h_in[N]; for (int i = 0; i < N; i++) { h_in[i] = float(i); } //定义GPU设备的数组 cudaArray *cu_Array; cudaMallocArray(&cu_Array, &textureRef.channelDesc, N, 1); //把数据复制到GPU的数组中 cudaMemcpyToArray(cu_Array, 0, 0, h_in, sizeof(float)*N, cudaMemcpyHostToDevice); // GPU数组绑定前面申请的纹理内存 cudaBindTextureToArray(textureRef, cu_Array); //调用Kernel函数,并运算 gpu_texture_memory << <num_blocks, NUM_THREADS >> > (N, d_out); // 把结果复制回主机以方便打印和处理 cudaMemcpy(h_out, d_out, sizeof(float)*N, cudaMemcpyDeviceToHost); printf("Use of Texture memory on GPU: \n"); for (int i = 0; i < N; i++) { printf("Texture element at %d is : %f\n", i, h_out[i]); } free(h_out); cudaFree(d_out); cudaFreeArray(cu_Array); cudaUnbindTexture(textureRef); }
纹理引用是通过texture<>类型的变量进行定义的。定义的时候,它具有3个参数:
第一个参数:是texture<>类型的变量定义时候的参数,用来说明纹理元素的类型。在本例中,是float类型;
第二个参数:说明了纹理引用的类型,可以是1D的,2D的,3D的。在本例中,是1D的纹理引用;
第三个参数:则是读取模式,这是一个可选参数,用来说明是否要执行读取时候的自动类型转换。
CUDA数组的使用与普通的数组类似,但是它却是纹理专用的。CUDA数组对于内核函数来说是只读的。但可以在主机上通过cudaMemcpyToArray函数写入,如同之前的代码中看到的那样。在cudaMemcpyToArray函数中,第二个和第三个参数中的0代表传输到的目标CUDA数组横向和纵向上的偏移量。两个方向上的偏移量都是0代表我们的这次传输将从目标CUDA数组的左上角(0,0)开始。CUDA数组中的存储器布局对用户来说是不透明的,这种布局对纹理拾取进行过特别优化。
cudaBindTextureToArray函数,将纹理引用和CUDA数组进行绑定。之前写入内容的CUDA数组将成为该纹理引用的后备存储。纹理引用绑定完成后调用内核,该内核将进行纹理拾取,同时将结果数据写入到显存中的目标数组。
注意:CUDA对于显存中常见的大数据量的存储方式有两种:
一种:是普通的线性存储,可以直接用指针访问。
另外一种:则是CUDA数组,对用户不透明,不能在内核里直接用指针访问,需要通过texture或者surface的相应函数进行访问。
本例的内核中,从texture reference进行的读取使用了相应的纹理拾取函数,而写入直接用普通的指针(d_out[])进行。当内核执行完成后,结果数组被复制回到主机上的内存中,然后在控制台窗口中显示出来。当使用完纹理存储后,我们需要执行解除绑定的代码,这是通过调用cudaUnbindTexture函数进行的。然后使用cudaFreeArray()函数释放刚才分配的CUDA数组空间。
注意:请一定要确保纹理引用被定义成全局静态变量,同时还要确保它不能作为参数传递给任何其他函数。在这个内核函数中,每个线程通过纹理引用读取自己线程ID作为索引位置的数据,然后复制到d_out指针指向的全局内存中。