CUDA编程之内存

CUDA的内存类型有全局内存、共享内存、常量内存、纹理内存、本地内存、寄存器等。我们需要分别了解它们的特点和使用场景。在CUDA编程中,合理利用各种内存类型对性能优化至关重要。

1. ‌**全局内存(Global Memory)**‌

  • 特点‌:设备中最大、最慢的内存,所有线程均可访问,需通过合并访问优化带宽。
  • 使用方法 ‌:
    • 分配与传输:

      复制代码
      float *d_data;
      cudaMalloc(&d_data, size); // 分配
      cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice); // 数据传输
    • 核函数访问:

      复制代码
      __global__ void kernel(float *data) {
          int idx = blockIdx.x * blockDim.x + threadIdx.x;
          data[idx] *= 2; // 合并访问(连续线程访问连续地址)
      }

2. ‌**共享内存(Shared Memory)**‌

  • 特点‌:块内线程共享,速度快,需避免Bank Conflict。
  • 使用方法 ‌:
    • 静态声明:

      复制代码
      __global__ void reduce0(float *g_in,float *g_out)
      {
          //每个线程从全局内存中加载一个对应位置元素到共享内存
          __shared__ float s_data[256]; //共享内存大小等于线程块的大小
          int tid = threadIdx.x;	//共享内存中的索引,即在线程块中的编号
          int i = blockIdx.x * blockDim.x + threadIdx.x; //全局内存中的索引
          s_data[tid] = g_in[i];
          __syncthreads();	//同步等待共享内存加载完毕
       
          //归约操作优化(避免Bank Conflict)
          //在共享内存做相邻配对归约,线程和数据序号一一对应
          for(int s = 1; s < blockDim.x; s *= 2) 
          {
              if(tid % (2 * s) == 0) 
              {
                  s_data[tid] += s_data[tid + s];
              }
              __syncthreads();
          }
       
          //把结果写回全局内存
          if (tid == 0) g_out[blockIdx.x] = s_data[0];
      }
      
      
      __global__ void reduce1(float *g_in,float *g_out)
      {
          //每个线程从全局内存中加载一个对应位置元素到共享内存
          __shared__ float s_data[256];//共享内存大小等于线程块的大小
          int tid = threadIdx.x;	//共享内存中的索引,即在线程块中的编号
          int i = blockIdx.x*blockDim.x + threadIdx.x;//全局内存中的索引
          s_data[tid] = g_in[i];
          __syncthreads();	//同步等待共享内存加载完毕
       
          //归约操作优化(避免Bank Conflict)
          //在共享内存做相邻配对归约,线程和数据序号间隔对应
          for(int s = 1; s < blockDim.x; s *= 2) 
          {
              int index = 2 * s * tid;
              if (index < blockDim.x) 
              {
                  s_data[index] += s_data[index + s];
              }
              __syncthreads();
          }
       
          //把结果写回全局内存
          if (tid == 0) g_out[blockIdx.x] = s_data[0];
      }
      
      __global__ void reduce2(float *g_in,float *g_out)
      {
          //每个线程从全局内存中加载一个对应位置元素到共享内存
          __shared__ float s_data[256];//共享内存大小等于线程块的大小
          int tid = threadIdx.x;	//共享内存中的索引,即在线程块中的编号
          int i = blockIdx.x * blockDim.x + threadIdx.x;//全局内存中的索引
          s_data[tid] = g_in[i];
          __syncthreads();	//同步等待共享内存加载完毕
       
          //归约操作优化(避免Bank Conflict)
          //在共享内存做交错配对归约
          for(int s = (blockDim.x >> 1); s > 0; s >>= 1) 
          {
              if (tid < s) 
              {
                  s_data[tid] += s_data[tid + s];
              }
              __syncthreads();
          }
       
          //把结果写回全局内存
          if (tid == 0) g_out[blockIdx.x] = s_data[0];
      }
    • 动态声明:

      复制代码
      extern __shared__ float sdata[]; // 核函数调用时指定大小:<<<grid, block, smem_size>>>

3. ‌**常量内存(Constant Memory)**‌

  • 特点‌:只读,适合频繁访问的常量数据,具有缓存优化。
  • 使用方法 ‌:
    • 声明与数据拷贝:

      复制代码
      __constant__ float const_data[1024];
      cudaMemcpyToSymbol(const_data, h_data, sizeof(float)*1024);
    • 核函数中直接访问:

      复制代码
      __global__ void kernel() {
          float value = const_data[threadIdx.x];
      }

4. ‌**纹理内存(Texture Memory)**‌

  • 特点‌:适合具有空间局部性的访问,如图像处理。

  • 纹理内存的寻址模式:

    寻址模式有几种:cudaAddressModeWrap、cudaAddressModeClamp、cudaAddressModeBorder、cudaAddressModeMirror

    cudaAddressModeWrap:循环模式,超出范围的坐标会循环到另一侧。比如,当x坐标超过宽度时,会回到起始位置,类似取模操作。

    cudaAddressModeClamp:防止数据外溢,越界坐标取边界值。

    cudaAddressModeBorder:严格限制数据范围,越界坐标返回零值‌。

    cudaAddressModeMirror:对称信号处理,越界坐标镜像对称。

  • 使用方法 ‌:

    • 创建纹理对象:

      复制代码
      texture<float, 1, cudaReadModeElementType> tex_ref;
      cudaArray *cuArray;
      cudaMallocArray(&cuArray, &tex_ref.channelDesc, size);
      cudaMemcpyToArray(cuArray, 0, 0, h_data, size, cudaMemcpyHostToDevice);
      cudaBindTextureToArray(tex_ref, cuArray);
    • 核函数采样:

      复制代码
      __global__ void kernel() {
          float value = tex1Dfetch(tex_ref, threadIdx.x);
      }

硬件插值功能实现代码:

核函数:

复制代码
//定义纹理内存变量
texture<float, cudaTextureType2D, cudaReadModeElementType>  tex_src;


__global__ void resize_img_ker(int row, int col, float x_a, float y_a, uchar *out)
{
  int x = threadIdx.x + blockDim.x * blockIdx.x;  //col
  int y = threadIdx.y + blockDim.y * blockIdx.y;  //row


  if (x < col && y < row)
  {
    float xx = x*x_a;
    float yy = y*y_a;
    //这里的xx和yy都是浮点数,tex2D函数返回的数值已经过硬件插值了,所以不需要开发者再进行插值啦~
    out[y*col+x] = (uchar)tex2D(tex_src, xx, yy);
  }
}

主体函数:

复制代码
void resize_img_cuda(Mat src, Mat &dst, float row_m, float col_m)
{
  const int row = (int)(src.rows*row_m);
  const int col = (int)(src.cols*col_m);
  const int srcimg_size = src.rows*src.cols*sizeof(float);
  const int dstimg_size = row*col;
  const float x_a = 1.0 / col_m;
  const float y_a = 1.0 / row_m;


  uchar *dst_cuda;
  cudaMalloc((void**)&dst_cuda, dstimg_size);


  Mat src_tmp;
  src.convertTo(src_tmp, CV_32F);  //注意这里要把图像转换为float浮点型,否则线性插值模式无法使用
  cudaChannelFormatDesc channelDesc = cudaCreateChannelDesc<float>();//声明数据类型
  cudaArray *cuArray_src;  //定义CUDA数组
  cudaMallocArray(&cuArray_src, &channelDesc, src_tmp.cols, src_tmp.rows);  //分配大小为col*row的CUDA数组
  tex_src.addressMode[0] = cudaAddressModeWrap;//寻址方式
  tex_src.addressMode[1] = cudaAddressModeWrap;//寻址方式 如果是三维数组则设置texRef.addressMode[2]
  tex_src.normalized = false;//是否对纹理坐标归一化
  tex_src.filterMode = cudaFilterModeLinear;//硬件插值方式:最邻近插值--cudaFilterModePoint 双线性插值--cudaFilterModeLinear
  cudaBindTextureToArray(&tex_src, cuArray_src, &channelDesc);  //把CUDA数组绑定到纹理内存
  cudaMemcpyToArray(cuArray_src, 0, 0, src_tmp.data, srcimg_size, cudaMemcpyHostToDevice);   //把源图像数据拷贝到CUDA数组


  dim3 Block_resize(16, 16);
  dim3 Grid_resize((col + 15) / 16, (row + 15) / 16);
  
  //调用核函数
  resize_img_ker << <Grid_resize, Block_resize >> > (row, col, x_a, y_a, dst_cuda);


  dst = Mat::zeros(row, col, CV_8UC1);
  cudaMemcpy(dst.data, dst_cuda, dstimg_size, cudaMemcpyDeviceToHost);


  cudaFree(dst_cuda);
  cudaFreeArray(cuArray_src);
  cudaUnbindTexture(tex_src);
}

5. ‌**本地内存(Local Memory)**‌

  • 特点‌:线程私有,速度慢,由编译器自动分配(如大数组或寄存器不足时)。

  • 优化 ‌:减少使用,优先使用寄存器或共享内存。

    复制代码
    __global__ void kernel() {
        float local_var; // 寄存器分配
        // float large_array[100]; // 可能溢出到本地内存
    }

6. ‌**寄存器(Registers)**‌

  • 特点 ‌:最快的内存,但数量有限。优化方法包括减少变量或循环展开。

    复制代码
    __global__ void kernel() {
        int tid = threadIdx.x; // 使用寄存器
        // 循环展开减少寄存器压力
        float sum = data + data + data + data;
    }

7. ‌**固定内存(Pinned Memory)**‌

  • 特点‌:主机内存,加速主机与设备间传输。

  • 使用方法 ‌:

    复制代码
    float *h_pinned;
    cudaHostAlloc(&h_pinned, size, cudaHostAllocDefault); // 分配固定内存
    cudaMemcpy(d_data, h_pinned, size, cudaMemcpyHostToDevice); // 快速传输

八、总结

内存类型 作用域 生命周期 速度 使用场景 注意事项
全局内存 所有线程 手动释放 最慢 大数据传输,跨线程块通信 需合并访问(连续地址访问)
共享内存 线程块内 线程块执行期间 线程块内协作(如归约、矩阵分块) 避免Bank Conflict(线程访问不同Bank)
常量内存 所有线程 程序结束 较快 频繁读取的常量数据(如配置参数) 只读,需提前用cudaMemcpyToSymbol拷贝
纹理内存 所有线程 手动释放 较快 具有空间局部性的访问(如图像采样) 需绑定纹理对象,支持插值和缓存
本地内存 单个线程 线程执行期间 寄存器溢出时的临时变量(如大数组) 尽量避免使用,优先用寄存器/共享内存
寄存器 单个线程 线程执行期间 最快 局部变量和临时计算 数量有限(每个线程约255个)
‌**固定内存(Pinned)**‌ 主机内存 手动释放 高带宽 主机与设备间快速数据传输(如流处理) 分配开销大,避免过量使用